001/*
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2023 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
034        JSON(Constants.CT_FHIR_JSON, Constants.CT_FHIR_JSON_NEW, Constants.FORMAT_JSON) {
035                @Override
036                public IParser newParser(FhirContext theContext) {
037                        return theContext.newJsonParser();
038                }
039        },
040
041        XML(Constants.CT_FHIR_XML, Constants.CT_FHIR_XML_NEW, Constants.FORMAT_XML) {
042                @Override
043                public IParser newParser(FhirContext theContext) {
044                        return theContext.newXmlParser();
045                }
046        },
047
048        RDF(Constants.CT_RDF_TURTLE_LEGACY, Constants.CT_RDF_TURTLE, Constants.FORMAT_TURTLE) {
049                @Override
050                public IParser newParser(FhirContext theContext) {
051                        return theContext.newRDFParser();
052                }
053        },
054
055        NDJSON(Constants.CT_FHIR_NDJSON, Constants.CT_FHIR_NDJSON, Constants.FORMAT_NDJSON) {
056                @Override
057                public IParser newParser(FhirContext theContext) {
058                        return theContext.newNDJsonParser();
059                }
060        };
061
062        /**
063         * "json"
064         */
065        public static final String JSON_PLAIN_STRING = "json";
066
067        /**
068         * "rdf"
069         */
070        public static final String RDF_PLAIN_STRING = "rdf";
071
072
073        /**
074         * "xml"
075         */
076        public static final String XML_PLAIN_STRING = "xml";
077
078        /**
079         * "ndjson"
080         */
081        public static final String NDJSON_PLAIN_STRING = "ndjson";
082        
083        private static Map<String, EncodingEnum> ourContentTypeToEncoding;
084        private static Map<String, EncodingEnum> ourContentTypeToEncodingLegacy;
085        private static Map<String, EncodingEnum> ourContentTypeToEncodingStrict;
086
087        static {
088                ourContentTypeToEncoding = new HashMap<>();
089                ourContentTypeToEncodingLegacy = new HashMap<>();
090
091                for (EncodingEnum next : values()) {
092                        ourContentTypeToEncoding.put(next.myResourceContentTypeNonLegacy, next);
093                        ourContentTypeToEncoding.put(next.myResourceContentTypeLegacy, next);
094                        ourContentTypeToEncodingLegacy.put(next.myResourceContentTypeLegacy, next);
095
096                        /*
097                         * See #346
098                         */
099                        ourContentTypeToEncoding.put(next.myResourceContentTypeNonLegacy.replace('+', ' '), next);
100                        ourContentTypeToEncoding.put(next.myResourceContentTypeLegacy.replace('+', ' '), next);
101                        ourContentTypeToEncodingLegacy.put(next.myResourceContentTypeLegacy.replace('+', ' '), next);
102
103                }
104
105                // Add before we add the lenient ones
106                ourContentTypeToEncodingStrict = Collections.unmodifiableMap(new HashMap<>(ourContentTypeToEncoding));
107
108                /*
109                 * These are wrong, but we add them just to be tolerant of other
110                 * people's mistakes
111                 */
112                ourContentTypeToEncoding.put("application/json", JSON);
113                ourContentTypeToEncoding.put("application/xml", XML);
114                ourContentTypeToEncoding.put("application/fhir+turtle", RDF);
115                ourContentTypeToEncoding.put("application/x-turtle", RDF);
116                ourContentTypeToEncoding.put("application/ndjson", NDJSON);
117                ourContentTypeToEncoding.put("text/json", JSON);
118                ourContentTypeToEncoding.put("text/ndjson", NDJSON);
119                ourContentTypeToEncoding.put("text/xml", XML);
120                ourContentTypeToEncoding.put("text/turtle", RDF);
121
122                /*
123                 * Plain values, used for parameter values
124                 */
125                ourContentTypeToEncoding.put(JSON_PLAIN_STRING, JSON);
126                ourContentTypeToEncoding.put(XML_PLAIN_STRING, XML);
127                ourContentTypeToEncoding.put(RDF_PLAIN_STRING, RDF);
128                ourContentTypeToEncoding.put(NDJSON_PLAIN_STRING, NDJSON);
129                ourContentTypeToEncoding.put(Constants.FORMAT_TURTLE, RDF);
130
131                ourContentTypeToEncodingLegacy = Collections.unmodifiableMap(ourContentTypeToEncodingLegacy);
132
133        }
134
135        private String myFormatContentType;
136        private String myResourceContentTypeLegacy;
137        private String myResourceContentTypeNonLegacy;
138
139        EncodingEnum(String theResourceContentTypeLegacy, String theResourceContentType, String theFormatContentType) {
140                myResourceContentTypeLegacy = theResourceContentTypeLegacy;
141                myResourceContentTypeNonLegacy = theResourceContentType;
142                myFormatContentType = theFormatContentType;
143        }
144
145        /**
146         * Returns <code>xml</code> or <code>json</code> as used on the <code>_format</code> search parameter
147         */
148        public String getFormatContentType() {
149                return myFormatContentType;
150        }
151
152        /**
153         * Will return application/xml+fhir style
154         */
155        public String getResourceContentType() {
156                return myResourceContentTypeLegacy;
157        }
158
159        /**
160         * Will return application/fhir+xml style
161         */
162        public String getResourceContentTypeNonLegacy() {
163                return myResourceContentTypeNonLegacy;
164        }
165
166        public abstract IParser newParser(final FhirContext theContext);
167
168        public static EncodingEnum detectEncoding(final String theBody) {
169                EncodingEnum retVal = detectEncodingNoDefault(theBody);
170                retVal = ObjectUtils.defaultIfNull(retVal, EncodingEnum.XML);
171                return retVal;
172        }
173
174        public static EncodingEnum detectEncodingNoDefault(String theBody) {
175                EncodingEnum retVal = null;
176                for (int i = 0; i < theBody.length() && retVal == null; i++) {
177                        switch (theBody.charAt(i)) {
178                                case '<':
179                                        retVal = EncodingEnum.XML;
180                                        break;
181                                case '{':
182                                        retVal = EncodingEnum.JSON;
183                                        break;
184                        }
185                }
186                return retVal;
187        }
188
189        /**
190         * Returns the encoding for a given content type, or <code>null</code> if no encoding
191         * is found.
192         * <p>
193         * <b>This method is lenient!</b> Things like "application/xml" will return {@link EncodingEnum#XML}
194         * even if the "+fhir" part is missing from the expected content type. Also,
195         * spaces are treated as a plus (i.e. "application/fhir json" will be treated as
196         * "application/fhir+json" in order to account for unescaped spaces in URL
197         * parameters)
198         * </p>
199         */
200        public static EncodingEnum forContentType(final String theContentType) {
201                String contentTypeSplitted = getTypeWithoutCharset(theContentType);
202                if (contentTypeSplitted == null) {
203                        return null;
204                } else {
205                        return ourContentTypeToEncoding.get(contentTypeSplitted );
206                }
207        }
208
209
210        /**
211         * Returns the encoding for a given content type, or <code>null</code> if no encoding
212         * is found.
213         * <p>
214         * <b>This method is NOT lenient!</b> Things like "application/xml" will return <code>null</code>
215         * </p>
216         *
217         * @see #forContentType(String)
218         */
219        public static EncodingEnum forContentTypeStrict(final String theContentType) {
220                String contentTypeSplitted = getTypeWithoutCharset(theContentType);
221                if (contentTypeSplitted == null) {
222                        return null;
223                } else {
224                        return ourContentTypeToEncodingStrict.get(contentTypeSplitted);
225                }
226        }
227
228        static String getTypeWithoutCharset(final String theContentType) {
229                if (isBlank(theContentType)) {
230                        return null;
231                } else {
232
233                        int start = 0;
234                        for (; start < theContentType.length(); start++) {
235                                if (theContentType.charAt(start) != ' ') {
236                                        break;
237                                }
238                        }
239                        int end = start;
240                        for (; end < theContentType.length(); end++) {
241                                if (theContentType.charAt(end) == ';') {
242                                        break;
243                                }
244                        }
245                        for (; end > start; end--) {
246                                if (theContentType.charAt(end - 1) != ' ') {
247                                        break;
248                                }
249                        }
250
251                        String retVal = theContentType.substring(start, end);
252
253                        if (retVal.contains(" ")) {
254                                retVal = retVal.replace(' ', '+');
255                        }
256                        return retVal;
257                }
258        }
259
260        /**
261         * Is the given type a FHIR legacy (pre-DSTU3) content type?
262         */
263        public static boolean isLegacy(final String theContentType) {
264                String contentTypeSplitted = getTypeWithoutCharset(theContentType);
265                if (contentTypeSplitted == null) {
266                        return false;
267                } else {
268                        return ourContentTypeToEncodingLegacy.containsKey(contentTypeSplitted);
269                }
270        }
271
272
273}