001/*- 002 * #%L 003 * Smile CDR - CDR 004 * %% 005 * Copyright (C) 2016 - 2026 Smile CDR, Inc. 006 * %% 007 * All rights reserved. 008 * #L% 009 */ 010package ca.cdr.api.consent; 011 012// Created by claude-opus-4-5-20251101 013 014import java.net.URLDecoder; 015import java.nio.charset.StandardCharsets; 016import java.util.ArrayList; 017import java.util.Collections; 018import java.util.HashMap; 019import java.util.List; 020import java.util.Map; 021 022/** 023 * Parser for consent policy names that may contain query parameters. 024 * Splits policy names on '?' and parses query parameters with URL decoding. 025 * 026 * <p>Examples: 027 * <ul> 028 * <li>{@code "RedactFhirPathsWhen?_type=Patient"} ? base="RedactFhirPathsWhen", params={_type:[Patient]}</li> 029 * <li>{@code "RedactFhirPathsWhen?a=b&a=c"} ? base="RedactFhirPathsWhen", params={a:[[b],[c]]}</li> 030 * <li>{@code "RedactFhirPathsWhen?a=b,c&a=d"} ? base="RedactFhirPathsWhen", params={a:[[b,c],[d]]}</li> 031 * </ul> 032 */ 033public class ParameterizedPolicyQueryParameterParser { 034 035 private final String myBasePolicyName; 036 private final Map<String, List<List<String>>> myParameters; 037 038 private ParameterizedPolicyQueryParameterParser( 039 String theBasePolicyName, Map<String, List<List<String>>> theParameters) { 040 myBasePolicyName = theBasePolicyName; 041 myParameters = theParameters; 042 } 043 044 /** 045 * Parse a policy name string that may contain query parameters. 046 * 047 * @param thePolicyNameWithParams the policy name, optionally followed by ?key=value&key2=value2 048 * @return a parsed result containing the base policy name and parameters 049 */ 050 public static ParameterizedPolicyQueryParameterParser parse(String thePolicyNameWithParams) { 051 if (thePolicyNameWithParams == null) { 052 return new ParameterizedPolicyQueryParameterParser(null, Collections.emptyMap()); 053 } 054 055 int questionMarkIndex = thePolicyNameWithParams.indexOf('?'); 056 if (questionMarkIndex == -1) { 057 // No query parameters 058 return new ParameterizedPolicyQueryParameterParser(thePolicyNameWithParams, Collections.emptyMap()); 059 } 060 061 String baseName = thePolicyNameWithParams.substring(0, questionMarkIndex); 062 String queryString = thePolicyNameWithParams.substring(questionMarkIndex + 1); 063 064 Map<String, List<List<String>>> params = parseQueryString(queryString); 065 066 return new ParameterizedPolicyQueryParameterParser(baseName, params); 067 } 068 069 private static Map<String, List<List<String>>> parseQueryString(String theQueryString) { 070 Map<String, List<List<String>>> result = new HashMap<>(); 071 072 if (theQueryString == null || theQueryString.isEmpty()) { 073 return result; 074 } 075 076 String[] pairs = theQueryString.split("&"); 077 for (String pair : pairs) { 078 if (pair.isEmpty()) { 079 continue; 080 } 081 082 int equalsIndex = pair.indexOf('='); 083 String key; 084 String value; 085 086 if (equalsIndex == -1) { 087 // Key without value 088 key = urlDecode(pair); 089 value = ""; 090 } else { 091 key = urlDecode(pair.substring(0, equalsIndex)); 092 value = urlDecode(pair.substring(equalsIndex + 1)); 093 } 094 095 // Split value on comma to create OR list 096 List<String> orList = splitAndTrim(value); 097 result.computeIfAbsent(key, k -> new ArrayList<>()).add(orList); 098 } 099 100 return result; 101 } 102 103 private static List<String> splitAndTrim(String theValue) { 104 if (theValue == null || theValue.isEmpty()) { 105 return Collections.singletonList(""); 106 } 107 List<String> result = new ArrayList<>(); 108 for (String part : theValue.split(",")) { 109 String trimmed = part.trim(); 110 result.add(trimmed); 111 } 112 return result; 113 } 114 115 private static String urlDecode(String theValue) { 116 try { 117 return URLDecoder.decode(theValue, StandardCharsets.UTF_8); 118 } catch (IllegalArgumentException e) { 119 // If decoding fails, return the original value 120 return theValue; 121 } 122 } 123 124 /** 125 * Get the base policy name (without query parameters). 126 * 127 * @return the base policy name 128 */ 129 public String getBasePolicyName() { 130 return myBasePolicyName; 131 } 132 133 /** 134 * Check if the policy has any parameters. 135 * 136 * @return true if parameters exist 137 */ 138 public boolean hasParameters() { 139 return !myParameters.isEmpty(); 140 } 141 142 /** 143 * Check if a specific parameter exists. 144 * 145 * @param theKey the parameter key 146 * @return true if the parameter exists 147 */ 148 public boolean hasParameter(String theKey) { 149 List<List<String>> andLists = myParameters.get(theKey); 150 return andLists != null && !andLists.isEmpty(); 151 } 152 153 /** 154 * Get the AND/OR structure for a parameter. 155 * The outer list represents AND groups (each occurrence of the parameter). 156 * The inner list represents OR values (comma-separated within one occurrence). 157 * 158 * @param theKey the parameter key 159 * @return list of AND groups, each containing OR values, or empty list if not found 160 */ 161 public List<List<String>> getParameter(String theKey) { 162 List<List<String>> andLists = myParameters.get(theKey); 163 return andLists != null ? Collections.unmodifiableList(andLists) : Collections.emptyList(); 164 } 165 166 /** 167 * Get the first value for a parameter. 168 * 169 * @param theKey the parameter key 170 * @return the first value from the first AND group, or null if not found 171 */ 172 public String getFirstParameterValue(String theKey) { 173 List<List<String>> andLists = myParameters.get(theKey); 174 if (andLists != null && !andLists.isEmpty()) { 175 List<String> firstOrList = andLists.get(0); 176 if (firstOrList != null && !firstOrList.isEmpty()) { 177 return firstOrList.get(0); 178 } 179 } 180 return null; 181 } 182 183 /** 184 * Get all values for a parameter, flattened across all AND groups. 185 * 186 * @param theKey the parameter key 187 * @return list of all values, or empty list if not found 188 * @deprecated Use {@link #getParameter(String)} to access the AND/OR structure 189 */ 190 @Deprecated 191 public List<String> getParameterValues(String theKey) { 192 List<List<String>> andLists = myParameters.get(theKey); 193 if (andLists == null || andLists.isEmpty()) { 194 return Collections.emptyList(); 195 } 196 List<String> flattened = new ArrayList<>(); 197 for (List<String> orList : andLists) { 198 flattened.addAll(orList); 199 } 200 return Collections.unmodifiableList(flattened); 201 } 202 203 /** 204 * Get all parameters with their AND/OR structure. 205 * 206 * @return unmodifiable map of parameter keys to lists of AND groups 207 */ 208 public Map<String, List<List<String>>> getAllParameters() { 209 return Collections.unmodifiableMap(myParameters); 210 } 211}