/****************************************************************************** * Product: iDempiere ERP & CRM Smart Business Solution * * This program is free software; you can redistribute it and/or modify it * * under the terms version 2 of the GNU General Public License as published * * by the Free Software Foundation. This program is distributed in the hope * * that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * * with this program; if not, write to the Free Software Foundation, Inc., * * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. * *****************************************************************************/ package org.adempiere.base; import java.sql.ResultSet; import java.util.HashMap; import java.util.Map; import java.util.function.BiConsumer; import org.compiere.model.PO; import org.compiere.util.CLogger; import org.osgi.framework.wiring.BundleWiring; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import io.github.classgraph.AnnotationInfo; import io.github.classgraph.ClassGraph; import io.github.classgraph.ClassGraph.ScanResultProcessor; import io.github.classgraph.ClassInfo; import io.github.classgraph.ClassInfoList; import io.github.classgraph.ScanResult; /** * Translates table names into model classes having the {@link Model} annotation. Relies on * {@link DefaultModelFactory} for everything else.
* This factory is designed to have a service rank higher than {@link DefaultModelFactory}, as class * discovery using SPI is preferred over reflection-based methods. * @author Saulo Gil * @author Heng Sin */ @Component(immediate = true, service = IModelFactory.class, property = {"service.ranking:Integer=0"}) public class AnnotationBasedModelFactory extends AnnotationBasedFactory implements IModelFactory { /** * Table name to class cache */ private Map> classCache = new HashMap<>(); private final static CLogger s_log = CLogger.getCLogger(AnnotationBasedModelFactory.class); /** * Packages to scan for Core X* classes */ private final static String[] CORE_PACKAGES = new String[] { "org.compiere.model", "compiere.model", "adempiere.model", "org.adempiere.model", "org.compiere.wf", "org.compiere.print", "org.compiere.impexp", "org.eevolution.model" }; /** * Extension point. Subclasses might override this method in order to have faster * model class scanning. * @return array of packages to be accepted during class scanning * @see ClassGraph#acceptPackagesNonRecursive(String...) */ protected String[] getPackages() { return new String[]{}; } /** * Extension point. Provide a list of patterns to match against class names. * @return array of strings containing patterns * @see ClassGraph#acceptClasses(String...) */ protected String[] getAcceptClassesPatterns() { String[] patterns = new String[] {"*.X_*","*.M*"}; return patterns; } /** * Scan annotation upon activation of component * @param context * @throws ClassNotFoundException */ @Activate public void activate(ComponentContext context) throws ClassNotFoundException { long start = System.currentTimeMillis(); ClassLoader classLoader = context.getBundleContext().getBundle().adapt(BundleWiring.class).getClassLoader(); ClassGraph graph = new ClassGraph() .enableAnnotationInfo() .overrideClassLoaders(classLoader) .disableNestedJarScanning() .disableModuleScanning(); // narrow search to a list of packages String[] packages = null; if(isAtCore()) packages = CORE_PACKAGES; else packages = getPackages(); //acceptClasses has no effect when acceptPackagesNonRecursive is use if (packages != null && packages.length > 0) { graph.acceptPackagesNonRecursive(packages); } else { // narrow search to class names matching a set of patterns String[] acceptClasses = getAcceptClassesPatterns(); if(acceptClasses!=null && acceptClasses.length > 0) graph.acceptClasses(acceptClasses); } ScanResultProcessor scanResultProcessor = scanResult -> { try { processResults(classLoader, scanResult); } catch (Exception e) { s_log.severe("exception found while scanning classes" + e.getMessage()); signalScanCompletion(false); return; } long end = System.currentTimeMillis(); s_log.info(() -> this.getClass().getSimpleName() + " loaded " + classCache.size() + " classes in " + ((end-start)/1000f) + "s"); signalScanCompletion(true); }; graph.scanAsync(getExecutorService(), getMaxThreads(), scanResultProcessor, getScanFailureHandler()); } /** * Process annotation scan result * @param classLoader * @param scanResult */ private void processResults(ClassLoader classLoader, ScanResult scanResult ) { BiConsumer exceptionHandler = (className, exception) -> s_log.severe(String.format("exception while loading class %s - %s", className, exception.getMessage())); for (ClassInfo classInfo : scanResult.getClassesWithAnnotation(Model.class)) { String className = classInfo.getName(); AnnotationInfo annotationInfo = classInfo.getAnnotationInfo(Model.class); String tableName = (String) annotationInfo.getParameterValues().getValue("table"); Class existing = classCache.get(tableName); // try to detect M classes only if we found an X class if(existing == null && className.substring(className.lastIndexOf(".")).startsWith(".X")) { ClassInfoList subclasses = classInfo.getSubclasses().directOnly(); while(!subclasses.isEmpty()) { className = subclasses.get(0).getName(); subclasses = subclasses.get(0).getSubclasses().directOnly(); } } if(existing==null) { try { classCache.put(tableName, classLoader.loadClass(className)); } catch (ClassNotFoundException e) { exceptionHandler.accept(className, e); } } else if (className.substring(className.lastIndexOf(".")).startsWith(".M")) { if(existing.getSimpleName().startsWith("X_")) { try { classCache.put(tableName, classLoader.loadClass(className)); } catch (ClassNotFoundException e) { exceptionHandler.accept(className, e); } } else { Class found = null; try { found = classLoader.loadClass(className); } catch (ClassNotFoundException e) { exceptionHandler.accept(className, e); } // replace existing entries only if found class has a lower hierarchy if(found != null && existing.isAssignableFrom(found)) try { classCache.put(tableName, classLoader.loadClass(className)); } catch (ClassNotFoundException e) { exceptionHandler.accept(className, e); } } } } } /** * {@inheritDoc} */ @Override public Class getClass(String tableName) { blockWhileScanning(); return classCache.get(tableName); } /** * Tells whether we're executing code from a subclass of {@link AnnotationBasedModelFactory}. * @return */ private boolean isAtCore() { return getClass().equals(AnnotationBasedModelFactory.class); } /** * {@inheritDoc} */ @Override public PO getPO(String tableName, int Record_ID, String trxName) { return AbstractModelFactory.getPO(getClass(tableName), tableName, Record_ID, trxName); } /** * {@inheritDoc} */ @Override public PO getPO(String tableName, String Record_UU, String trxName) { return AbstractModelFactory.getPO(getClass(tableName), tableName, Record_UU, trxName); } /** * {@inheritDoc} */ @Override public PO getPO(String tableName, ResultSet rs, String trxName) { return AbstractModelFactory.getPO(getClass(tableName), tableName, rs, trxName); } }