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.ConfigurationException;
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
025import jakarta.annotation.Nonnull;
026import org.apache.commons.lang3.Validate;
027
028import java.lang.reflect.Constructor;
029import java.lang.reflect.Field;
030import java.lang.reflect.InvocationTargetException;
031import java.lang.reflect.Method;
032import java.lang.reflect.Modifier;
033import java.lang.reflect.ParameterizedType;
034import java.lang.reflect.Type;
035import java.lang.reflect.TypeVariable;
036import java.lang.reflect.WildcardType;
037import java.util.ArrayList;
038import java.util.Arrays;
039import java.util.Comparator;
040import java.util.HashMap;
041import java.util.List;
042import java.util.concurrent.ConcurrentHashMap;
043
044public class ReflectionUtil {
045
046        public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
047        public static final Class<?>[] EMPTY_CLASS_ARRAY = new Class[0];
048        private static final ConcurrentHashMap<String, Object> ourFhirServerVersions = new ConcurrentHashMap<>();
049        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ReflectionUtil.class);
050
051        /**
052         * Non instantiable
053         */
054        private ReflectionUtil() {
055                super();
056        }
057
058        /**
059         * Returns all methods declared against {@literal theClazz}. This method returns a predictable order, which is
060         * sorted by method name and then by parameters.
061         * <p>
062         * This method does not include superclass methods (see {@link #getDeclaredMethods(Class, boolean)} if you
063         * want to include those.
064         * </p>
065         */
066        public static List<Method> getDeclaredMethods(Class<?> theClazz) {
067                return getDeclaredMethods(theClazz, false);
068        }
069
070        /**
071         * Returns all methods declared against {@literal theClazz}. This method returns a predictable order, which is
072         * sorted by method name and then by parameters.
073         */
074        public static List<Method> getDeclaredMethods(Class<?> theClazz, boolean theIncludeMethodsFromSuperclasses) {
075                HashMap<String, Method> foundMethods = new HashMap<>();
076
077                populateDeclaredMethodsMap(theClazz, foundMethods, theIncludeMethodsFromSuperclasses);
078
079                List<Method> retVal = new ArrayList<>(foundMethods.values());
080                retVal.sort((Comparator.comparing(ReflectionUtil::describeMethodInSortFriendlyWay)));
081                return retVal;
082        }
083
084        private static void populateDeclaredMethodsMap(
085                        Class<?> theClazz, HashMap<String, Method> foundMethods, boolean theIncludeMethodsFromSuperclasses) {
086                Method[] declaredMethods = theClazz.getDeclaredMethods();
087                for (Method next : declaredMethods) {
088
089                        if (Modifier.isAbstract(next.getModifiers())
090                                        || Modifier.isStatic(next.getModifiers())
091                                        || Modifier.isPrivate(next.getModifiers())) {
092                                continue;
093                        }
094
095                        String description = next.getName() + Arrays.asList(next.getParameterTypes());
096
097                        if (!foundMethods.containsKey(description)) {
098                                try {
099                                        Method method = theClazz.getMethod(next.getName(), next.getParameterTypes());
100                                        foundMethods.put(description, method);
101                                } catch (NoSuchMethodException | SecurityException e) {
102                                        foundMethods.put(description, next);
103                                }
104                        }
105                }
106
107                if (theIncludeMethodsFromSuperclasses && !theClazz.getSuperclass().equals(Object.class)) {
108                        populateDeclaredMethodsMap(theClazz.getSuperclass(), foundMethods, theIncludeMethodsFromSuperclasses);
109                }
110        }
111
112        /**
113         * Returns a description like <code>startsWith params(java.lang.String, int) returns(boolean)</code>.
114         * The format is chosen in order to provide a predictable and useful sorting order.
115         */
116        public static String describeMethodInSortFriendlyWay(Method theMethod) {
117                StringBuilder b = new StringBuilder();
118                b.append(theMethod.getName());
119                b.append(" returns(");
120                b.append(theMethod.getReturnType().getName());
121                b.append(") params(");
122                Class<?>[] parameterTypes = theMethod.getParameterTypes();
123                for (int i = 0, parameterTypesLength = parameterTypes.length; i < parameterTypesLength; i++) {
124                        if (i > 0) {
125                                b.append(", ");
126                        }
127                        Class<?> next = parameterTypes[i];
128                        b.append(next.getName());
129                }
130                b.append(")");
131                return b.toString();
132        }
133
134        public static Class<?> getGenericCollectionTypeOfField(Field next) {
135                ParameterizedType collectionType = (ParameterizedType) next.getGenericType();
136                return getGenericCollectionTypeOf(collectionType.getActualTypeArguments()[0]);
137        }
138
139        /**
140         * For a field of type List<Enumeration<Foo>>, returns Foo
141         */
142        public static Class<?> getGenericCollectionTypeOfFieldWithSecondOrderForList(Field next) {
143                if (!List.class.isAssignableFrom(next.getType())) {
144                        return getGenericCollectionTypeOfField(next);
145                }
146
147                Class<?> type;
148                ParameterizedType collectionType = (ParameterizedType) next.getGenericType();
149                Type firstArg = collectionType.getActualTypeArguments()[0];
150                if (ParameterizedType.class.isAssignableFrom(firstArg.getClass())) {
151                        ParameterizedType pt = ((ParameterizedType) firstArg);
152                        Type pt2 = pt.getActualTypeArguments()[0];
153                        return (Class<?>) pt2;
154                }
155                type = (Class<?>) firstArg;
156                return type;
157        }
158
159        public static Class<?> getGenericCollectionTypeOfMethodParameter(Method theMethod, int theParamIndex) {
160                Type genericParameterType = theMethod.getGenericParameterTypes()[theParamIndex];
161                if (Class.class.equals(genericParameterType) || Class.class.equals(genericParameterType.getClass())) {
162                        return null;
163                }
164                ParameterizedType collectionType = (ParameterizedType) genericParameterType;
165                return getGenericCollectionTypeOf(collectionType.getActualTypeArguments()[0]);
166        }
167
168        public static Class<?> getGenericCollectionTypeOfMethodReturnType(Method theMethod) {
169                Type genericReturnType = theMethod.getGenericReturnType();
170                if (!(genericReturnType instanceof ParameterizedType)) {
171                        return null;
172                }
173                ParameterizedType collectionType = (ParameterizedType) genericReturnType;
174                return getGenericCollectionTypeOf(collectionType.getActualTypeArguments()[0]);
175        }
176
177        @SuppressWarnings({"rawtypes"})
178        private static Class<?> getGenericCollectionTypeOf(Type theType) {
179                Class<?> type;
180                if (ParameterizedType.class.isAssignableFrom(theType.getClass())) {
181                        ParameterizedType pt = ((ParameterizedType) theType);
182                        type = (Class<?>) pt.getRawType();
183                } else if (theType instanceof TypeVariable<?>) {
184                        Type decl = ((TypeVariable) theType).getBounds()[0];
185                        return (Class<?>) decl;
186                } else if (theType instanceof WildcardType) {
187                        Type decl = ((WildcardType) theType).getUpperBounds()[0];
188                        return (Class<?>) decl;
189                } else {
190                        type = (Class<?>) theType;
191                }
192                return type;
193        }
194
195        public static boolean isInstantiable(Class<?> theType) {
196                return !theType.isInterface() && !Modifier.isAbstract(theType.getModifiers());
197        }
198
199        /**
200         * Instantiate a class by no-arg constructor, throw {@link ConfigurationException} if we fail to do so
201         */
202        public static <T> T newInstance(Class<T> theType) {
203                Validate.notNull(theType, "theType must not be null");
204                try {
205                        return theType.getConstructor().newInstance();
206                } catch (Exception e) {
207                        throw new ConfigurationException(Msg.code(1784) + "Failed to instantiate " + theType.getName(), e);
208                }
209        }
210
211        public static <T> T newInstance(Class<T> theType, Class<?> theArgumentType, Object theArgument) {
212                Validate.notNull(theType, "theType must not be null");
213                try {
214                        Constructor<T> constructor = theType.getConstructor(theArgumentType);
215                        return constructor.newInstance(theArgument);
216                } catch (Exception e) {
217                        throw new ConfigurationException(Msg.code(1785) + "Failed to instantiate " + theType.getName(), e);
218                }
219        }
220
221        public static Object newInstanceOfFhirServerType(String theType) {
222                String errorMessage =
223                                "Unable to instantiate server framework. Please make sure that hapi-fhir-server library is on your classpath!";
224                String wantedType = "ca.uhn.fhir.rest.api.server.IFhirVersionServer";
225                return newInstanceOfType(theType, theType, errorMessage, wantedType, new Class[0], new Object[0]);
226        }
227
228        private static Object newInstanceOfType(
229                        String theKey,
230                        String theType,
231                        String errorMessage,
232                        String wantedType,
233                        Class<?>[] theParameterArgTypes,
234                        Object[] theConstructorArgs) {
235                Object fhirServerVersion = ourFhirServerVersions.get(theKey);
236                if (fhirServerVersion == null) {
237                        try {
238                                Class<?> type = Class.forName(theType);
239                                Class<?> serverType = Class.forName(wantedType);
240                                Validate.isTrue(serverType.isAssignableFrom(type));
241                                fhirServerVersion = type.getConstructor(theParameterArgTypes).newInstance(theConstructorArgs);
242                        } catch (Exception e) {
243                                throw new ConfigurationException(Msg.code(1786) + errorMessage, e);
244                        }
245
246                        ourFhirServerVersions.put(theKey, fhirServerVersion);
247                }
248                return fhirServerVersion;
249        }
250
251        public static <T> T newInstanceOrReturnNull(String theClassName, Class<T> theType) {
252                return newInstanceOrReturnNull(theClassName, theType, EMPTY_CLASS_ARRAY, EMPTY_OBJECT_ARRAY);
253        }
254
255        @SuppressWarnings("unchecked")
256        public static <T> T newInstanceOrReturnNull(
257                        String theClassName, Class<T> theType, Class<?>[] theArgTypes, Object[] theArgs) {
258                try {
259                        return newInstance(theClassName, theType, theArgTypes, theArgs);
260                } catch (ConfigurationException e) {
261                        throw e;
262                } catch (Exception e) {
263                        ourLog.info("Failed to instantiate {}: {}", theClassName, e.toString());
264                        return null;
265                }
266        }
267
268        @Nonnull
269        public static <T> T newInstance(String theClassName, Class<T> theType, Class<?>[] theArgTypes, Object[] theArgs) {
270                try {
271                        Class<?> clazz = Class.forName(theClassName);
272                        if (!theType.isAssignableFrom(clazz)) {
273                                throw new ConfigurationException(Msg.code(1787) + theClassName + " is not assignable to " + theType);
274                        }
275                        return (T) clazz.getConstructor(theArgTypes).newInstance(theArgs);
276                } catch (ClassNotFoundException
277                                | NoSuchMethodException
278                                | InstantiationException
279                                | IllegalAccessException
280                                | InvocationTargetException e) {
281                        throw new InternalErrorException(Msg.code(2330) + e.getMessage(), e);
282                }
283        }
284
285        public static boolean typeExists(String theName) {
286                try {
287                        Class.forName(theName);
288                        return true;
289                } catch (ClassNotFoundException theE) {
290                        return false;
291                }
292        }
293}