/*********************************************************************** * This file is part of iDempiere ERP Open Source * * http://www.idempiere.org * * * * Copyright (C) Contributors * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 2 * * of the License, or (at your option) any later version. * * * * 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., 51 Franklin Street, Fifth Floor, Boston, * * MA 02110-1301, USA. * * * * Contributors: * * - hengsin * **********************************************************************/ package org.adempiere.base; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import org.adempiere.base.annotation.Callout; import org.adempiere.base.annotation.Callouts; import org.compiere.util.CLogger; import org.osgi.framework.BundleContext; import org.osgi.framework.wiring.BundleWiring; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; import io.github.classgraph.AnnotationInfo; import io.github.classgraph.AnnotationInfoList; import io.github.classgraph.ClassGraph; import io.github.classgraph.ClassGraph.ScanResultProcessor; import io.github.classgraph.ClassInfo; /** * Abstract base class for annotation driven implementation of {@link IColumnCalloutFactory}.
* Subclass would override the {@link #getPackages()} method to provide the packages for {@link Callout} annotation scanning and discovery. * @author hengsin */ public abstract class AnnotationBasedColumnCalloutFactory extends AnnotationBasedFactory implements IColumnCalloutFactory { private final static CLogger s_log = CLogger.getCLogger(AnnotationBasedColumnCalloutFactory.class); private BundleContext bundleContext; private final Map>> tableNameMap = new HashMap<>(); private final Map[]> constructorCache = new ConcurrentHashMap<>(); public AnnotationBasedColumnCalloutFactory() { } @Override public IColumnCallout[] getColumnCallouts(String tableName, String columnName) { blockWhileScanning(); List callouts = new ArrayList(); ClassLoader classLoader = bundleContext.getBundle().adapt(BundleWiring.class).getClassLoader(); Map> columnNameMap = tableNameMap.get(tableName); if (columnNameMap != null) { List calloutClassNames = columnNameMap.get(columnName); if (calloutClassNames != null) { newCalloutInstance(callouts, classLoader, calloutClassNames); } calloutClassNames = columnNameMap.get("*"); if (calloutClassNames != null) { newCalloutInstance(callouts, classLoader, calloutClassNames); } } columnNameMap = tableNameMap.get("*"); if (columnNameMap != null) { List calloutClassNames = columnNameMap.get(columnName); if (calloutClassNames != null) { newCalloutInstance(callouts, classLoader, calloutClassNames); } calloutClassNames = columnNameMap.get("*"); if (calloutClassNames != null) { newCalloutInstance(callouts, classLoader, calloutClassNames); } } return callouts.toArray(new IColumnCallout[0]); } /** * Create new callout instance using reflection and add it to the callouts list * @param callouts * @param classLoader * @param calloutClassNames */ private void newCalloutInstance(List callouts, ClassLoader classLoader, List calloutClassNames) { for(String calloutClass : calloutClassNames) { Constructor[] constructors = constructorCache.get(calloutClass); if (constructors == null) { try { Class clazz = classLoader.loadClass(calloutClass); Constructor constructor = clazz.getDeclaredConstructor(); IColumnCallout columnCallout = (IColumnCallout) constructor.newInstance(); callouts.add(columnCallout); constructors = new Constructor[] {constructor}; constructorCache.put(calloutClass, constructors); } catch (Exception e) { s_log.log(Level.WARNING, e.getMessage(), e); constructors = new Constructor[0]; constructorCache.put(calloutClass, constructors); } } else if (constructors.length == 1){ try { IColumnCallout columnCallout = (IColumnCallout) constructors[0].newInstance(); callouts.add(columnCallout); } catch (Exception e) { s_log.log(Level.WARNING, e.getMessage(), e); constructors = new Constructor[0]; constructorCache.put(calloutClass, constructors); } } } } /** * Subclasses must override this method in order to provide packages to * scan, discover and register {@link IColumnCallout} classes * @return array of packages to be accepted during class scanning * @see ClassGraph#acceptPackagesNonRecursive(String...) */ protected abstract String[] getPackages(); /** * Perform annotation scanning upon activation of component * @param context * @throws ClassNotFoundException */ @Activate public void activate(ComponentContext context) throws ClassNotFoundException { long start = System.currentTimeMillis(); bundleContext = context.getBundleContext(); ClassLoader classLoader = bundleContext.getBundle().adapt(BundleWiring.class).getClassLoader(); ClassGraph graph = new ClassGraph() .enableAnnotationInfo() .overrideClassLoaders(classLoader) .disableNestedJarScanning() .disableModuleScanning() .acceptPackagesNonRecursive(getPackages()); ScanResultProcessor scanResultProcessor = scanResult -> { /** * It's necessary to check if a class has already been processed to avoid duplicate callout registration, * because sometimes scanResult returns ClassInfo with both Callout and Callouts annotations for the same class, * as in the case of CalloutInfoWindow. */ List processed = new ArrayList(); for (ClassInfo classInfo : scanResult.getClassesWithAnnotation(Callouts.class)) { if (classInfo.isAbstract()) continue; String className = classInfo.getName(); /** * scenario 1: return list with 1 element of AnnotationInfo of type Callouts. * scenario 2: (CalloutInfoWindow), return list AnnotationInfo of type Callout. */ AnnotationInfoList annotInfos = classInfo.getAnnotationInfo(); for (AnnotationInfo annotInfo : annotInfos) { if (Callout.class.getName().equals(annotInfo.getName())) { processAnnotation(className, annotInfo); }else if (Callouts.class.getName().equals(annotInfo.getName())) { // Declaring repeated @Callout annotations is treated as @Callouts(value = Callout[]). String calloutsRepeatablePropertiesName = "value"; Object[] calloutAnnotInfos = (Object[])annotInfo.getParameterValues().getValue(calloutsRepeatablePropertiesName); for (Object calloutAnnotInfo : calloutAnnotInfos) { processAnnotation(className, (AnnotationInfo)calloutAnnotInfo); } } } processed.add(className); } for (ClassInfo classInfo : scanResult.getClassesWithAnnotation(Callout.class)) { if (classInfo.isAbstract()) continue; String className = classInfo.getName(); if (processed.contains(className)) continue; AnnotationInfo annotationInfo = classInfo.getAnnotationInfo(Callout.class); processAnnotation(className, annotationInfo); } signalScanCompletion(true); long end = System.currentTimeMillis(); s_log.info(() -> this.getClass().getSimpleName() + " loaded "+tableNameMap.size() +" classes in " + ((end-start)/1000f) + "s"); }; graph.scanAsync(getExecutorService(), getMaxThreads(), scanResultProcessor, getScanFailureHandler()); } /** * Process class annotation and register column callout. * @param className * @param annotationInfo */ private void processAnnotation(String className, AnnotationInfo annotationInfo) { //not sure why but sometime ClassGraph return Object[] instead of the expected String[] Object[] tableNames = (Object[]) annotationInfo.getParameterValues().getValue("tableName"); Object[] columnNames = (Object[]) annotationInfo.getParameterValues().getValue("columnName"); boolean matchAllTables = false; for(Object tableName : tableNames) { if ("*".equals(tableName) ) { matchAllTables = true; break; } } boolean matchAllColumns = false; for(Object columnName : columnNames) { if ("*".equals(columnName)) { matchAllColumns = true; break; } } //not allow to match everything if (matchAllColumns && matchAllTables) return; Map> columnNameMap = null; if (matchAllTables) { columnNameMap = tableNameMap.get("*"); if (columnNameMap == null) { columnNameMap = new HashMap>(); tableNameMap.put("*", columnNameMap); } if (matchAllColumns) { addCallout(className, columnNameMap); } else { addCallout(className, columnNames, columnNameMap); } } else { for(Object tableName : tableNames) { columnNameMap = tableNameMap.get(tableName); if (columnNameMap == null) { columnNameMap = new HashMap>(); tableNameMap.put((String)tableName, columnNameMap); } if (matchAllColumns) { addCallout(className, columnNameMap); } else { addCallout(className, columnNames, columnNameMap); } } } } /** * add callout for column names * @param className * @param columnNames * @param columnNameMap */ private void addCallout(String className, Object[] columnNames, Map> columnNameMap) { for (Object columnName : columnNames) { List callouts = columnNameMap.get(columnName); if (callouts == null ) { callouts = new ArrayList(); columnNameMap.put((String)columnName, callouts); } callouts.add(className); } } /** * add global callout (for all columns) * @param className * @param columnNameMap */ private void addCallout(String className, Map> columnNameMap) { List callouts = columnNameMap.get("*"); if (callouts == null ) { callouts = new ArrayList(); columnNameMap.put("*", callouts); } callouts.add(className); } }