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.util;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.i18n.Msg;
024import org.apache.commons.lang3.StringUtils;
025import org.hl7.fhir.instance.model.api.IBase;
026
027import java.lang.reflect.Method;
028import java.util.Arrays;
029import java.util.List;
030import java.util.stream.Collectors;
031
032/**
033 * Helper class for handling updates of the instances that support property modification via <code>setProperty</code>
034 * and <code>getProperty</code> methods.
035 */
036public class PropertyModifyingHelper {
037
038        public static final String GET_PROPERTY_METHOD_NAME = "getProperty";
039        public static final String SET_PROPERTY_METHOD_NAME = "setProperty";
040        public static final String DEFAULT_DELIMITER = ", ";
041
042        private IBase myBase;
043
044        private String myDelimiter = DEFAULT_DELIMITER;
045
046        private FhirContext myFhirContext;
047
048        /**
049         * Creates a new instance initializing the dependencies.
050         *
051         * @param theFhirContext FHIR context holding the resource definitions
052         * @param theBase        The base class to set properties on
053         */
054        public PropertyModifyingHelper(FhirContext theFhirContext, IBase theBase) {
055                if (findGetPropertyMethod(theBase) == null) {
056                        throw new IllegalArgumentException(
057                                        Msg.code(1771) + "Specified base instance does not support property retrieval.");
058                }
059                myBase = theBase;
060                myFhirContext = theFhirContext;
061        }
062
063        /**
064         * Gets the method with the specified name and parameter types.
065         *
066         * @param theObject       Non-null instance to get the method from
067         * @param theMethodName   Name of the method to get
068         * @param theParamClasses Parameters types that method parameters should be assignable as
069         * @return Returns the method with the given name and parameters or null if it can't be found
070         */
071        protected Method getMethod(Object theObject, String theMethodName, Class... theParamClasses) {
072                for (Method m : theObject.getClass().getDeclaredMethods()) {
073                        if (m.getName().equals(theMethodName)) {
074                                if (theParamClasses.length == 0) {
075                                        return m;
076                                }
077                                if (m.getParameterCount() != theParamClasses.length) {
078                                        continue;
079                                }
080                                for (int i = 0; i < theParamClasses.length; i++) {
081                                        if (!m.getParameterTypes()[i].isAssignableFrom(theParamClasses[i])) {
082                                                continue;
083                                        }
084                                }
085                                return m;
086                        }
087                }
088                return null;
089        }
090
091        /**
092         * Gets all non-blank fields as a single string joined with the delimiter provided by {@link #getDelimiter()}
093         *
094         * @param theFiledNames Field names to retrieve values for
095         * @return Returns all specified non-blank fileds as a single string.
096         */
097        public String getFields(String... theFiledNames) {
098                return Arrays.stream(theFiledNames)
099                                .map(this::get)
100                                .filter(s -> !StringUtils.isBlank(s))
101                                .collect(Collectors.joining(getDelimiter()));
102        }
103
104        /**
105         * Gets property with the specified name from the provided base class.
106         *
107         * @param thePropertyName Name of the property to get
108         * @return Returns property value converted to string. In case of multiple values, they are joined with the
109         * specified delimiter.
110         */
111        public String get(String thePropertyName) {
112                return getMultiple(thePropertyName).stream().collect(Collectors.joining(getDelimiter()));
113        }
114
115        /**
116         * Sets property or adds to a collection of properties with the specified name from the provided base class.
117         *
118         * @param thePropertyName Name of the property to set or add element to in case property is a collection
119         */
120        public void set(String thePropertyName, String theValue) {
121                if (theValue == null || theValue.isEmpty()) {
122                        return;
123                }
124
125                try {
126                        IBase value = myFhirContext.getElementDefinition("string").newInstance(theValue);
127                        Method setPropertyMethod = findSetPropertyMethod(myBase, int.class, String.class, value.getClass());
128                        int hashCode = thePropertyName.hashCode();
129                        setPropertyMethod.invoke(myBase, hashCode, thePropertyName, value);
130                } catch (Exception e) {
131                        throw new IllegalStateException(
132                                        Msg.code(1772) + String.format("Unable to set property %s on %s", thePropertyName, myBase), e);
133                }
134        }
135
136        /**
137         * Gets property values with the specified name from the provided base class.
138         *
139         * @param thePropertyName Name of the property to get
140         * @return Returns property values converted to string.
141         */
142        public List<String> getMultiple(String thePropertyName) {
143                Method getPropertyMethod = findGetPropertyMethod(myBase);
144                Object[] values;
145                try {
146                        values = (Object[]) getPropertyMethod.invoke(myBase, thePropertyName.hashCode(), thePropertyName, true);
147                } catch (Exception e) {
148                        throw new IllegalStateException(
149                                        Msg.code(1773) + String.format("Instance %s does not supply property %s", myBase, thePropertyName),
150                                        e);
151                }
152
153                return Arrays.stream(values)
154                                .map(String::valueOf)
155                                .filter(s -> !StringUtils.isEmpty(s))
156                                .collect(Collectors.toList());
157        }
158
159        private Method findGetPropertyMethod(IBase theAddress) {
160                return getMethod(theAddress, GET_PROPERTY_METHOD_NAME);
161        }
162
163        private Method findSetPropertyMethod(IBase theAddress, Class... theParamClasses) {
164                return getMethod(theAddress, SET_PROPERTY_METHOD_NAME, theParamClasses);
165        }
166
167        /**
168         * Gets the delimiter used when concatenating multiple field values
169         *
170         * @return Returns the delimiter
171         */
172        public String getDelimiter() {
173                return myDelimiter;
174        }
175
176        /**
177         * Sets the delimiter used when concatenating multiple field values
178         *
179         * @param theDelimiter The delimiter to set
180         */
181        public void setDelimiter(String theDelimiter) {
182                this.myDelimiter = theDelimiter;
183        }
184
185        /**
186         * Gets the base instance that this helper operates on
187         *
188         * @return Returns the base instance
189         */
190        public IBase getBase() {
191                return myBase;
192        }
193}