001/*
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.rest.api;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.parser.IParser;
024import org.apache.commons.lang3.ObjectUtils;
025
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.Map;
029
030import static org.apache.commons.lang3.StringUtils.isBlank;
031
032public enum EncodingEnum {
033        JSON(Constants.CT_FHIR_JSON, Constants.CT_FHIR_JSON_NEW, Constants.FORMAT_JSON) {
034                @Override
035                public IParser newParser(FhirContext theContext) {
036                        return theContext.newJsonParser();
037                }
038        },
039
040        XML(Constants.CT_FHIR_XML, Constants.CT_FHIR_XML_NEW, Constants.FORMAT_XML) {
041                @Override
042                public IParser newParser(FhirContext theContext) {
043                        return theContext.newXmlParser();
044                }
045        },
046
047        RDF(Constants.CT_RDF_TURTLE_LEGACY, Constants.CT_RDF_TURTLE, Constants.FORMAT_TURTLE) {
048                @Override
049                public IParser newParser(FhirContext theContext) {
050                        return theContext.newRDFParser();
051                }
052        },
053
054        NDJSON(Constants.CT_FHIR_NDJSON, Constants.CT_FHIR_NDJSON, Constants.FORMAT_NDJSON) {
055                @Override
056                public IParser newParser(FhirContext theContext) {
057                        return theContext.newNDJsonParser();
058                }
059        };
060
061        /**
062         * "json"
063         */
064        public static final String JSON_PLAIN_STRING = "json";
065
066        /**
067         * "rdf"
068         */
069        public static final String RDF_PLAIN_STRING = "rdf";
070
071        /**
072         * "xml"
073         */
074        public static final String XML_PLAIN_STRING = "xml";
075
076        /**
077         * "ndjson"
078         */
079        public static final String NDJSON_PLAIN_STRING = "ndjson";
080
081        private static Map<String, EncodingEnum> ourContentTypeToEncoding;
082        private static Map<String, EncodingEnum> ourContentTypeToEncodingLegacy;
083        private static Map<String, EncodingEnum> ourContentTypeToEncodingStrict;
084
085        static {
086                ourContentTypeToEncoding = new HashMap<>();
087                ourContentTypeToEncodingLegacy = new HashMap<>();
088
089                for (EncodingEnum next : values()) {
090                        ourContentTypeToEncoding.put(next.myResourceContentTypeNonLegacy, next);
091                        ourContentTypeToEncoding.put(next.myResourceContentTypeLegacy, next);
092                        ourContentTypeToEncodingLegacy.put(next.myResourceContentTypeLegacy, next);
093
094                        /*
095                         * See #346
096                         */
097                        ourContentTypeToEncoding.put(next.myResourceContentTypeNonLegacy.replace('+', ' '), next);
098                        ourContentTypeToEncoding.put(next.myResourceContentTypeLegacy.replace('+', ' '), next);
099                        ourContentTypeToEncodingLegacy.put(next.myResourceContentTypeLegacy.replace('+', ' '), next);
100                }
101
102                // Add before we add the lenient ones
103                ourContentTypeToEncodingStrict = Collections.unmodifiableMap(new HashMap<>(ourContentTypeToEncoding));
104
105                /*
106                 * These are wrong, but we add them just to be tolerant of other
107                 * people's mistakes
108                 */
109                ourContentTypeToEncoding.put("application/json", JSON);
110                ourContentTypeToEncoding.put("application/xml", XML);
111                ourContentTypeToEncoding.put("application/fhir+turtle", RDF);
112                ourContentTypeToEncoding.put("application/x-turtle", RDF);
113                ourContentTypeToEncoding.put("application/ndjson", NDJSON);
114                ourContentTypeToEncoding.put("text/json", JSON);
115                ourContentTypeToEncoding.put("text/ndjson", NDJSON);
116                ourContentTypeToEncoding.put("text/xml", XML);
117                ourContentTypeToEncoding.put("text/turtle", RDF);
118
119                /*
120                 * Plain values, used for parameter values
121                 */
122                ourContentTypeToEncoding.put(JSON_PLAIN_STRING, JSON);
123                ourContentTypeToEncoding.put(XML_PLAIN_STRING, XML);
124                ourContentTypeToEncoding.put(RDF_PLAIN_STRING, RDF);
125                ourContentTypeToEncoding.put(NDJSON_PLAIN_STRING, NDJSON);
126                ourContentTypeToEncoding.put(Constants.FORMAT_TURTLE, RDF);
127
128                ourContentTypeToEncodingLegacy = Collections.unmodifiableMap(ourContentTypeToEncodingLegacy);
129        }
130
131        private String myFormatContentType;
132        private String myResourceContentTypeLegacy;
133        private String myResourceContentTypeNonLegacy;
134
135        EncodingEnum(String theResourceContentTypeLegacy, String theResourceContentType, String theFormatContentType) {
136                myResourceContentTypeLegacy = theResourceContentTypeLegacy;
137                myResourceContentTypeNonLegacy = theResourceContentType;
138                myFormatContentType = theFormatContentType;
139        }
140
141        /**
142         * Returns <code>xml</code> or <code>json</code> as used on the <code>_format</code> search parameter
143         */
144        public String getFormatContentType() {
145                return myFormatContentType;
146        }
147
148        /**
149         * Will return application/xml+fhir style
150         */
151        public String getResourceContentType() {
152                return myResourceContentTypeLegacy;
153        }
154
155        /**
156         * Will return application/fhir+xml style
157         */
158        public String getResourceContentTypeNonLegacy() {
159                return myResourceContentTypeNonLegacy;
160        }
161
162        public abstract IParser newParser(final FhirContext theContext);
163
164        public static EncodingEnum detectEncoding(final String theBody) {
165                EncodingEnum retVal = detectEncodingNoDefault(theBody);
166                retVal = ObjectUtils.defaultIfNull(retVal, EncodingEnum.XML);
167                return retVal;
168        }
169
170        public static EncodingEnum detectEncodingNoDefault(String theBody) {
171                EncodingEnum retVal = null;
172                for (int i = 0; i < theBody.length() && retVal == null; i++) {
173                        switch (theBody.charAt(i)) {
174                                case '<':
175                                        retVal = EncodingEnum.XML;
176                                        break;
177                                case '{':
178                                        retVal = EncodingEnum.JSON;
179                                        break;
180                        }
181                }
182                return retVal;
183        }
184
185        /**
186         * Returns the encoding for a given content type, or <code>null</code> if no encoding
187         * is found.
188         * <p>
189         * <b>This method is lenient!</b> Things like "application/xml" will return {@link EncodingEnum#XML}
190         * even if the "+fhir" part is missing from the expected content type. Also,
191         * spaces are treated as a plus (i.e. "application/fhir json" will be treated as
192         * "application/fhir+json" in order to account for unescaped spaces in URL
193         * parameters)
194         * </p>
195         */
196        public static EncodingEnum forContentType(final String theContentType) {
197                String contentTypeSplitted = getTypeWithoutCharset(theContentType);
198                if (contentTypeSplitted == null) {
199                        return null;
200                } else {
201                        return ourContentTypeToEncoding.get(contentTypeSplitted);
202                }
203        }
204
205        /**
206         * Returns the encoding for a given content type, or <code>null</code> if no encoding
207         * is found.
208         * <p>
209         * <b>This method is NOT lenient!</b> Things like "application/xml" will return <code>null</code>
210         * </p>
211         *
212         * @see #forContentType(String)
213         */
214        public static EncodingEnum forContentTypeStrict(final String theContentType) {
215                String contentTypeSplitted = getTypeWithoutCharset(theContentType);
216                if (contentTypeSplitted == null) {
217                        return null;
218                } else {
219                        return ourContentTypeToEncodingStrict.get(contentTypeSplitted);
220                }
221        }
222
223        static String getTypeWithoutCharset(final String theContentType) {
224                if (isBlank(theContentType)) {
225                        return null;
226                } else {
227
228                        int start = 0;
229                        for (; start < theContentType.length(); start++) {
230                                if (theContentType.charAt(start) != ' ') {
231                                        break;
232                                }
233                        }
234                        int end = start;
235                        for (; end < theContentType.length(); end++) {
236                                if (theContentType.charAt(end) == ';') {
237                                        break;
238                                }
239                        }
240                        for (; end > start; end--) {
241                                if (theContentType.charAt(end - 1) != ' ') {
242                                        break;
243                                }
244                        }
245
246                        String retVal = theContentType.substring(start, end);
247
248                        if (retVal.contains(" ")) {
249                                retVal = retVal.replace(' ', '+');
250                        }
251                        return retVal;
252                }
253        }
254
255        /**
256         * Is the given type a FHIR legacy (pre-DSTU3) content type?
257         */
258        public static boolean isLegacy(final String theContentType) {
259                String contentTypeSplitted = getTypeWithoutCharset(theContentType);
260                if (contentTypeSplitted == null) {
261                        return false;
262                } else {
263                        return ourContentTypeToEncodingLegacy.containsKey(contentTypeSplitted);
264                }
265        }
266}