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