org.openmrs.module.metadatasharing.reflection.OpenmrsClassScanner.java Source code

Java tutorial

Introduction

Here is the source code for org.openmrs.module.metadatasharing.reflection.OpenmrsClassScanner.java

Source

/**
 * The contents of this file are subject to the OpenMRS Public License
 * Version 1.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://license.openmrs.org
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 *
 * Copyright (C) OpenMRS, LLC.  All Rights Reserved.
 */
package org.openmrs.module.metadatasharing.reflection;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openmrs.ConceptDatatype;
import org.openmrs.OpenmrsMetadata;
import org.openmrs.PersonAttributeType;
import org.openmrs.RelationshipType;
import org.openmrs.api.APIException;
import org.openmrs.api.OpenmrsService;
import org.openmrs.api.PersonService;
import org.openmrs.api.context.Context;
import org.openmrs.module.ModuleUtil;
import org.openmrs.module.metadatasharing.MetadataSharingConsts;
import org.openmrs.util.OpenmrsClassLoader;
import org.openmrs.util.OpenmrsConstants;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.stereotype.Component;

/**
 * Reflection utilities to search the classpath for OpenmrsMetadata classes and corresponding
 * services.
 * <p>
 * Search results are cached with a time-out set to {@link #CACHE_TIMEOUT_IN_MS}.
 */
@Component(MetadataSharingConsts.MODULE_ID + ".OpenmrsClassScanner")
public class OpenmrsClassScanner {

    protected final Log log = LogFactory.getLog(getClass());

    private static final OpenmrsClassScanner instance = new OpenmrsClassScanner();

    private final MetadataReaderFactory metadataReaderFactory;

    private final ResourcePatternResolver resourceResolver;

    private final ClassLoader classLoader;

    private Map<Class<?>, ClassMethod<OpenmrsService>> serviceSaveMethodsCache;

    private List<Class<OpenmrsMetadata>> metadataTypesCache;

    private Date metadataTypesCacheTimeout;

    private Date serviceSaveMethodsCacheTimeout;

    private long CACHE_TIMEOUT_IN_MS = 600000;

    private class ClassMethod<T> {

        public final Class<? extends T> service;

        public final Method method;

        public ClassMethod(Class<? extends T> service, Method method) {
            this.service = service;
            this.method = method;
        }
    }

    OpenmrsClassScanner() {
        this.classLoader = OpenmrsClassLoader.getInstance();
        this.metadataReaderFactory = new SimpleMetadataReaderFactory(classLoader);
        this.resourceResolver = new PathMatchingResourcePatternResolver(classLoader);
    }

    /**
     * @return the instance
     */
    public static OpenmrsClassScanner getInstance() {
        return instance;
    }

    /**
     * Searches for classes extending or implementing the given type.
     * 
     * @param <T>
     * @param type
     * @param concrete true if only concrete classes should be returned
     * @return the list of found classes
     * @throws IOException
     */
    public <T> List<Class<T>> getClasses(Class<? extends T> type, boolean concrete) throws IOException {
        Set<Class<T>> types = new LinkedHashSet<Class<T>>();
        String pattern = "classpath*:org/openmrs/**/*.class";
        Resource[] resources = resourceResolver.getResources(pattern);
        TypeFilter typeFilter = new AssignableTypeFilter(type);
        for (Resource resource : resources) {
            try {
                MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource);
                if (typeFilter.match(metadataReader, metadataReaderFactory)) {
                    if (concrete == metadataReader.getClassMetadata().isConcrete()) {
                        String classname = metadataReader.getClassMetadata().getClassName();
                        try {
                            @SuppressWarnings("unchecked")
                            Class<T> metadata = (Class<T>) OpenmrsClassLoader.getInstance().loadClass(classname);
                            types.add(metadata);
                        } catch (ClassNotFoundException e) {
                            throw new IOException("Class cannot be loaded: " + classname, e);
                        }
                    }
                }
            } catch (IOException e) {
                log.debug("Resource cannot be loaded: " + resource);
            }
        }
        return new ArrayList<Class<T>>(types);
    }

    /**
     * Searches for classes implementing {@link OpenmrsMetadata}.
     * <p>
     * The search result is cached for {@link #CACHE_TIMEOUT_IN_MS}.
     * 
     * @return the list of classes
     * @throws IOException
     */
    public List<Class<OpenmrsMetadata>> getOpenmrsMetadataClasses() throws IOException {
        if (metadataTypesCache == null || metadataTypesCacheTimeout.before(new Date())) {
            log.debug("Refreshing OpenmrsMetadata classes cache");
            long future = new Date().getTime();
            future += CACHE_TIMEOUT_IN_MS;
            metadataTypesCacheTimeout = new Date(future);
            metadataTypesCache = getClasses(OpenmrsMetadata.class, true);
            for (Iterator<Class<OpenmrsMetadata>> i = metadataTypesCache.iterator(); i.hasNext();) {
                if (!canBeSaved(i.next())) {
                    i.remove();
                }
            }
        }
        return new ArrayList<Class<OpenmrsMetadata>>(metadataTypesCache);
    }

    /**
     * Invokes service's save method for the given item.
     * <p>
     * The list of service classes is cached for {@link #CACHE_TIMEOUT_IN_MS}. It is always the
     * first matching save method that is invoked. If there is a match for the given item, the
     * persisted item is returned, otherwise <code>null</code> is returned.
     * 
     * @param <T>
     * @param item
     * @return the persisted item or <code>null</code> if the item was not saved
     * @throws IOException
     */
    public <T> T serviceSaveItem(T item) throws IOException {
        initServiceSaveMethodsCache();

        T result = invokeServiceSaveItem(item);
        if (result != null) {
            return result;
        }
        return null;
    }

    public boolean hasServiceSaveMethod(Class<?> type) throws IOException {
        initServiceSaveMethodsCache();
        return (serviceSaveMethodsCache.get(type) != null);
    }

    protected void initServiceSaveMethodsCache() throws IOException {
        if (serviceSaveMethodsCache != null && serviceSaveMethodsCacheTimeout.after(new Date())) {
            return;
        }
        log.debug("Refreshing OpenmrsService classes cache");
        long future = new Date().getTime();
        future += CACHE_TIMEOUT_IN_MS;
        serviceSaveMethodsCacheTimeout = new Date(future);
        serviceSaveMethodsCache = new HashMap<Class<?>, ClassMethod<OpenmrsService>>();

        List<Class<OpenmrsService>> services = getClasses(OpenmrsService.class, false);
        for (Class<OpenmrsService> service : services) {
            Method[] methods = service.getMethods();
            for (Method method : methods) {
                if (method.getName().startsWith("save")) {
                    if (Arrays.equals(method.getParameterTypes(), new Class<?>[] { method.getReturnType() })) {
                        if (canBeSaved(method.getReturnType())) {
                            serviceSaveMethodsCache.put(method.getReturnType(),
                                    new ClassMethod<OpenmrsService>(service, method));
                        }
                    }
                }
            }
        }
        //META-114 HACK
        try {
            serviceSaveMethodsCache.put(PersonAttributeType.class, new ClassMethod(PersonService.class,
                    PersonService.class.getMethod("savePersonAttributeType", PersonAttributeType.class)));
            serviceSaveMethodsCache.put(RelationshipType.class, new ClassMethod(PersonService.class,
                    PersonService.class.getMethod("saveRelationshipType", RelationshipType.class)));
        } catch (Exception ex) {
        }
    }

    /**
      * Currently this only handles the case where starting in OpenMRS 1.9 you are no longer allowed to modify
      * a ConceptDatatype.
      * 
      * @param clazz
      * @return true for all cases except ConceptDatatype on OpenMRS 1.9+
      */
    private boolean canBeSaved(Class<?> clazz) {
        if (clazz.equals(ConceptDatatype.class)
                && ModuleUtil.compareVersion(OpenmrsConstants.OPENMRS_VERSION_SHORT, "1.9") >= 0) {
            return false;
        } else {
            return true;
        }
    }

    protected <T> T invokeServiceSaveItem(T item) throws APIException {
        ClassMethod<OpenmrsService> classMethod = serviceSaveMethodsCache.get(item.getClass());
        if (classMethod == null) {
            classMethod = serviceSaveMethodsCache.get(item.getClass().getSuperclass());
        }

        if (classMethod != null) {
            log.debug("Getting service " + classMethod.service.getName());

            //try block is for META-114 -- casting to OpenmrsService fails because PersonService is not an OpenmrsService
            OpenmrsService openmrsService = null;
            try {
                openmrsService = Context.getService(classMethod.service);
            } catch (Exception ex) {
                if (item.getClass().equals(PersonAttributeType.class)
                        || item.getClass().equals(RelationshipType.class)) {
                    try {
                        @SuppressWarnings("unchecked")
                        T result = (T) classMethod.method.invoke(Context.getPersonService(), item);
                        log.debug("Invoking " + classMethod.method + "() suceeded");
                        return result;
                    } catch (Exception exInner) {
                        throw new APIException("Cannot invoke " + classMethod.method + "()", exInner);
                    }
                }
            }
            if (openmrsService != null) {
                log.debug("Getting service " + classMethod.service.getName() + " suceeded");
                try {
                    log.debug("Invoking " + classMethod.method + "() in service " + classMethod.method.getName());
                    @SuppressWarnings("unchecked")
                    T result = (T) classMethod.method.invoke(openmrsService, item);
                    log.debug("Invoking " + classMethod.method + "() suceeded");
                    return result;
                } catch (IllegalArgumentException e) {
                    throw new APIException("Cannot invoke " + classMethod.method + "()", e);
                } catch (IllegalAccessException e) {
                    throw new APIException("Cannot invoke " + classMethod.method + "()", e);
                } catch (InvocationTargetException e) {
                    throw new APIException("Cannot save " + item.toString()
                            + ". Failed with the following exception:\n\n" + ExceptionUtils.getFullStackTrace(e));
                }
            } else {
                log.debug("Getting service " + classMethod.service.getName() + " not suceeded");
            }
        }
        return null;
    }
}