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}