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