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 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("Specified base instance does not support property retrieval.");
057                }
058                myBase = theBase;
059                myFhirContext = theFhirContext;
060        }
061
062        /**
063         * Gets the method with the specified name and parameter types.
064         *
065         * @param theObject       Non-null instance to get the method from
066         * @param theMethodName   Name of the method to get
067         * @param theParamClasses Parameters types that method parameters should be assignable as
068         * @return Returns the method with the given name and parameters or null if it can't be found
069         */
070        protected Method getMethod(Object theObject, String theMethodName, Class... theParamClasses) {
071                for (Method m : theObject.getClass().getDeclaredMethods()) {
072                        if (m.getName().equals(theMethodName)) {
073                                if (theParamClasses.length == 0) {
074                                        return m;
075                                }
076                                if (m.getParameterCount() != theParamClasses.length) {
077                                        continue;
078                                }
079                                for (int i = 0; i < theParamClasses.length; i++) {
080                                        if (!m.getParameterTypes()[i].isAssignableFrom(theParamClasses[i])) {
081                                                continue;
082                                        }
083                                }
084                                return m;
085                        }
086                }
087                return null;
088        }
089
090        /**
091         * Gets all non-blank fields as a single string joined with the delimiter provided by {@link #getDelimiter()}
092         *
093         * @param theFiledNames Field names to retrieve values for
094         * @return Returns all specified non-blank fileds as a single string.
095         */
096        public String getFields(String... theFiledNames) {
097                return Arrays.stream(theFiledNames)
098                        .map(this::get)
099                        .filter(s -> !StringUtils.isBlank(s))
100                        .collect(Collectors.joining(getDelimiter()));
101        }
102
103        /**
104         * Gets property with the specified name from the provided base class.
105         *
106         * @param thePropertyName Name of the property to get
107         * @return Returns property value converted to string. In case of multiple values, they are joined with the
108         * specified delimiter.
109         */
110        public String get(String thePropertyName) {
111                return getMultiple(thePropertyName)
112                        .stream()
113                        .collect(Collectors.joining(getDelimiter()));
114        }
115
116        /**
117         * Sets property or adds to a collection of properties with the specified name from the provided base class.
118         *
119         * @param thePropertyName Name of the property to set or add element to in case property is a collection
120         */
121        public void set(String thePropertyName, String theValue) {
122                if (theValue == null || theValue.isEmpty()) {
123                        return;
124                }
125
126                try {
127                        IBase value = myFhirContext.getElementDefinition("string").newInstance(theValue);
128                        Method setPropertyMethod = findSetPropertyMethod(myBase, int.class, String.class, value.getClass());
129                        int hashCode = thePropertyName.hashCode();
130                        setPropertyMethod.invoke(myBase, hashCode, thePropertyName, value);
131                } catch (Exception e) {
132                        throw new IllegalStateException(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(String.format("Instance %s does not supply property %s", myBase, thePropertyName), e);
149                }
150
151                return Arrays.stream(values)
152                        .map(String::valueOf)
153                        .filter(s -> !StringUtils.isEmpty(s))
154                        .collect(Collectors.toList());
155        }
156
157        private Method findGetPropertyMethod(IBase theAddress) {
158                return getMethod(theAddress, GET_PROPERTY_METHOD_NAME);
159        }
160
161        private Method findSetPropertyMethod(IBase theAddress, Class... theParamClasses) {
162                return getMethod(theAddress, SET_PROPERTY_METHOD_NAME, theParamClasses);
163        }
164
165        /**
166         * Gets the delimiter used when concatenating multiple field values
167         *
168         * @return Returns the delimiter
169         */
170        public String getDelimiter() {
171                return myDelimiter;
172        }
173
174        /**
175         * Sets the delimiter used when concatenating multiple field values
176         *
177         * @param theDelimiter The delimiter to set
178         */
179        public void setDelimiter(String theDelimiter) {
180                this.myDelimiter = theDelimiter;
181        }
182
183        /**
184         * Gets the base instance that this helper operates on
185         *
186         * @return Returns the base instance
187         */
188        public IBase getBase() {
189                return myBase;
190        }
191}