/***********************************************************************
* 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);
}
}