001/*-
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2025 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.util;
021
022import ca.uhn.fhir.i18n.Msg;
023import ca.uhn.fhir.model.api.IModelJson;
024import ca.uhn.fhir.model.api.annotation.SensitiveNoDisplay;
025import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
026import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
027import com.fasterxml.jackson.annotation.JsonInclude;
028import com.fasterxml.jackson.core.JsonGenerator;
029import com.fasterxml.jackson.core.JsonProcessingException;
030import com.fasterxml.jackson.databind.ObjectMapper;
031import com.fasterxml.jackson.databind.SerializationFeature;
032import com.fasterxml.jackson.databind.SerializerProvider;
033import com.fasterxml.jackson.databind.ser.FilterProvider;
034import com.fasterxml.jackson.databind.ser.PropertyWriter;
035import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
036import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
037import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
038import jakarta.annotation.Nonnull;
039import org.thymeleaf.util.Validate;
040
041import java.io.IOException;
042import java.io.InputStream;
043import java.io.StringWriter;
044import java.io.Writer;
045import java.util.List;
046
047public class JsonUtil {
048
049        private static final ObjectMapper ourMapperPrettyPrint;
050        private static final ObjectMapper ourMapperNonPrettyPrint;
051        private static final ObjectMapper ourMapperIncludeSensitive;
052
053        public static final SimpleBeanPropertyFilter SIMPLE_BEAN_PROPERTY_FILTER = new SensitiveDataFilter();
054
055        public static final SimpleFilterProvider SENSITIVE_DATA_FILTER_PROVIDER =
056                        new SimpleFilterProvider().addFilter(IModelJson.SENSITIVE_DATA_FILTER_NAME, SIMPLE_BEAN_PROPERTY_FILTER);
057        public static final SimpleFilterProvider SHOW_ALL_DATA_FILTER_PROVIDER = new SimpleFilterProvider()
058                        .addFilter(IModelJson.SENSITIVE_DATA_FILTER_NAME, SimpleBeanPropertyFilter.serializeAll());
059
060        static {
061                ourMapperPrettyPrint = new ObjectMapper();
062                ourMapperPrettyPrint.setSerializationInclusion(JsonInclude.Include.NON_NULL);
063                ourMapperPrettyPrint.setFilterProvider(SENSITIVE_DATA_FILTER_PROVIDER);
064                ourMapperPrettyPrint.enable(SerializationFeature.INDENT_OUTPUT);
065                // Needed to handle ZonedDateTime
066                ourMapperPrettyPrint.registerModule(new JavaTimeModule());
067
068                ourMapperNonPrettyPrint = new ObjectMapper();
069                ourMapperNonPrettyPrint.setSerializationInclusion(JsonInclude.Include.NON_NULL);
070                ourMapperNonPrettyPrint.setFilterProvider(SENSITIVE_DATA_FILTER_PROVIDER);
071                ourMapperNonPrettyPrint.disable(SerializationFeature.INDENT_OUTPUT);
072                // Needed to handle ZonedDateTime
073                ourMapperNonPrettyPrint.registerModule(new JavaTimeModule());
074
075                ourMapperIncludeSensitive = new ObjectMapper();
076                ourMapperIncludeSensitive.setFilterProvider(SHOW_ALL_DATA_FILTER_PROVIDER);
077                ourMapperIncludeSensitive.setSerializationInclusion(JsonInclude.Include.NON_NULL);
078                ourMapperIncludeSensitive.disable(SerializationFeature.INDENT_OUTPUT);
079                // Needed to handle ZonedDateTime
080                ourMapperIncludeSensitive.registerModule(new JavaTimeModule());
081        }
082
083        /**
084         * Parse JSON
085         */
086        public static <T> T deserialize(@Nonnull String theInput, @Nonnull Class<T> theType) {
087                Validate.notEmpty(theInput, "theInput must not be blank or null");
088                try {
089                        return ourMapperPrettyPrint.readerFor(theType).readValue(theInput);
090                } catch (IOException e) {
091                        // Should not happen
092                        throw new InternalErrorException(Msg.code(2060) + e);
093                }
094        }
095
096        /**
097         * Parse JSON
098         */
099        public static <T> List<T> deserializeList(@Nonnull String theInput, @Nonnull Class<T> theType) throws IOException {
100                return ourMapperPrettyPrint.readerForListOf(theType).readValue(theInput);
101        }
102        /**
103         * Parse JSON
104         */
105        public static <T> T deserialize(@Nonnull InputStream theInput, @Nonnull Class<T> theType) throws IOException {
106                return ourMapperPrettyPrint.readerFor(theType).readValue(theInput);
107        }
108
109        /**
110         * Includes fields which are annotated with {@link SensitiveNoDisplay}. Currently only meant to be used for serialization
111         * for batch job parameters.
112         */
113        public static String serializeWithSensitiveData(@Nonnull IModelJson theInput) {
114                try {
115                        return ourMapperIncludeSensitive.writeValueAsString(theInput);
116                } catch (JsonProcessingException e) {
117                        throw new InvalidRequestException(Msg.code(2487) + "Failed to encode " + theInput.getClass(), e);
118                }
119        }
120
121        /**
122         * Encode JSON
123         */
124        public static String serialize(@Nonnull Object theInput) {
125                return serialize(theInput, true);
126        }
127
128        /**
129         * Encode JSON
130         */
131        public static String serialize(@Nonnull Object theInput, boolean thePrettyPrint) {
132                try {
133                        StringWriter sw = new StringWriter();
134                        if (thePrettyPrint) {
135                                ourMapperPrettyPrint.writeValue(sw, theInput);
136                        } else {
137                                ourMapperNonPrettyPrint.writeValue(sw, theInput);
138                        }
139                        return sw.toString();
140                } catch (IOException e) {
141                        // Should not happen
142                        throw new InternalErrorException(Msg.code(2061) + e);
143                }
144        }
145
146        public FilterProvider getSensitiveDataFilterProvider() {
147                return SENSITIVE_DATA_FILTER_PROVIDER;
148        }
149
150        /**
151         * Encode JSON
152         */
153        public static void serialize(@Nonnull Object theInput, @Nonnull Writer theWriter) throws IOException {
154                // Note: We append a string here rather than just having ourMapper write directly
155                // to the Writer because ourMapper seems to close the writer for some stupid
156                // reason.. There's probably a way of preventing that bit I'm not sure what that
157                // is and it's not a big deal here.
158                theWriter.append(serialize(theInput));
159        }
160
161        public static String serializeOrInvalidRequest(IModelJson theJson) {
162                try {
163                        return ourMapperNonPrettyPrint.writeValueAsString(theJson);
164                } catch (JsonProcessingException e) {
165                        throw newInvalidRequestException(theJson, e);
166                }
167        }
168
169        public static String serializeOrInvalidRequest(Object theObject) {
170                return serializeOrInvalidRequest(theObject, true);
171        }
172
173        public static String serializeOrInvalidRequest(Object theObject, boolean thePrettyPrint) {
174                try {
175                        return serialize(theObject, thePrettyPrint);
176                } catch (InternalErrorException e) {
177                        throw newInvalidRequestException(theObject, e);
178                }
179        }
180
181        private static InvalidRequestException newInvalidRequestException(Object theObject, Exception theCause)
182                        throws InvalidRequestException {
183                return new InvalidRequestException(Msg.code(1741) + "Failed to encode " + theObject.getClass(), theCause);
184        }
185
186        private static class SensitiveDataFilter extends SimpleBeanPropertyFilter {
187
188                @Override
189                protected boolean include(PropertyWriter writer) {
190                        return true; // Default include all except explicitly checked and excluded
191                }
192
193                @Override
194                public void serializeAsField(Object pojo, JsonGenerator gen, SerializerProvider provider, PropertyWriter writer)
195                                throws Exception {
196                        if (include(writer)) {
197                                if (!isFieldSensitive(writer)) {
198                                        super.serializeAsField(pojo, gen, provider, writer);
199                                }
200                        }
201                }
202
203                private boolean isFieldSensitive(PropertyWriter writer) {
204                        return writer.getAnnotation(SensitiveNoDisplay.class) != null;
205                }
206        }
207}