001package ca.uhn.fhir.util;
002
003/*-
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.context.FhirContext;
024import ca.uhn.fhir.i18n.Msg;
025import org.apache.commons.lang3.StringUtils;
026import org.hl7.fhir.instance.model.api.IBase;
027
028import java.lang.reflect.Method;
029import java.util.Arrays;
030import java.util.List;
031import java.util.stream.Collectors;
032
033/**
034 * Helper class for handling updates of the instances that support property modification via <code>setProperty</code>
035 * and <code>getProperty</code> methods.
036 */
037public class PropertyModifyingHelper {
038
039        public static final String GET_PROPERTY_METHOD_NAME = "getProperty";
040        public static final String SET_PROPERTY_METHOD_NAME = "setProperty";
041        public static final String DEFAULT_DELIMITER = ", ";
042
043        private IBase myBase;
044
045        private String myDelimiter = DEFAULT_DELIMITER;
046
047        private FhirContext myFhirContext;
048
049        /**
050         * Creates a new instance initializing the dependencies.
051         *
052         * @param theFhirContext FHIR context holding the resource definitions
053         * @param theBase        The base class to set properties on
054         */
055        public PropertyModifyingHelper(FhirContext theFhirContext, IBase theBase) {
056                if (findGetPropertyMethod(theBase) == null) {
057                        throw new IllegalArgumentException(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)
113                        .stream()
114                        .collect(Collectors.joining(getDelimiter()));
115        }
116
117        /**
118         * Sets property or adds to a collection of properties with the specified name from the provided base class.
119         *
120         * @param thePropertyName Name of the property to set or add element to in case property is a collection
121         */
122        public void set(String thePropertyName, String theValue) {
123                if (theValue == null || theValue.isEmpty()) {
124                        return;
125                }
126
127                try {
128                        IBase value = myFhirContext.getElementDefinition("string").newInstance(theValue);
129                        Method setPropertyMethod = findSetPropertyMethod(myBase, int.class, String.class, value.getClass());
130                        int hashCode = thePropertyName.hashCode();
131                        setPropertyMethod.invoke(myBase, hashCode, thePropertyName, value);
132                } catch (Exception e) {
133                        throw new IllegalStateException(Msg.code(1772) + String.format("Unable to set property %s on %s", thePropertyName, myBase), e);
134                }
135        }
136
137        /**
138         * Gets property values with the specified name from the provided base class.
139         *
140         * @param thePropertyName Name of the property to get
141         * @return Returns property values converted to string.
142         */
143        public List<String> getMultiple(String thePropertyName) {
144                Method getPropertyMethod = findGetPropertyMethod(myBase);
145                Object[] values;
146                try {
147                        values = (Object[]) getPropertyMethod.invoke(myBase, thePropertyName.hashCode(), thePropertyName, true);
148                } catch (Exception e) {
149                        throw new IllegalStateException(Msg.code(1773) + String.format("Instance %s does not supply property %s", myBase, thePropertyName), e);
150                }
151
152                return Arrays.stream(values)
153                        .map(String::valueOf)
154                        .filter(s -> !StringUtils.isEmpty(s))
155                        .collect(Collectors.toList());
156        }
157
158        private Method findGetPropertyMethod(IBase theAddress) {
159                return getMethod(theAddress, GET_PROPERTY_METHOD_NAME);
160        }
161
162        private Method findSetPropertyMethod(IBase theAddress, Class... theParamClasses) {
163                return getMethod(theAddress, SET_PROPERTY_METHOD_NAME, theParamClasses);
164        }
165
166        /**
167         * Gets the delimiter used when concatenating multiple field values
168         *
169         * @return Returns the delimiter
170         */
171        public String getDelimiter() {
172                return myDelimiter;
173        }
174
175        /**
176         * Sets the delimiter used when concatenating multiple field values
177         *
178         * @param theDelimiter The delimiter to set
179         */
180        public void setDelimiter(String theDelimiter) {
181                this.myDelimiter = theDelimiter;
182        }
183
184        /**
185         * Gets the base instance that this helper operates on
186         *
187         * @return Returns the base instance
188         */
189        public IBase getBase() {
190                return myBase;
191        }
192}