diff --git a/org.adempiere.base/src/org/idempiere/model/IMappedModelFactory.java b/org.adempiere.base/src/org/idempiere/model/IMappedModelFactory.java index d5623a1b94..8dc3be8777 100644 --- a/org.adempiere.base/src/org/idempiere/model/IMappedModelFactory.java +++ b/org.adempiere.base/src/org/idempiere/model/IMappedModelFactory.java @@ -29,6 +29,7 @@ import java.util.function.BiFunction; import java.util.function.Supplier; import org.compiere.model.PO; +import org.osgi.framework.BundleContext; /** * @@ -54,4 +55,10 @@ public interface IMappedModelFactory { */ void removeMapping(String tableName); + /** + * Scan packages for class with {@link org.adempiere.base.Model} annotation and add mapping for it + * @param context + * @param packages + */ + public void scan(BundleContext context, String... packages); } \ No newline at end of file diff --git a/org.adempiere.base/src/org/idempiere/model/MappedModelFactory.java b/org.adempiere.base/src/org/idempiere/model/MappedModelFactory.java index 6f36bd1505..79609fd86a 100644 --- a/org.adempiere.base/src/org/idempiere/model/MappedModelFactory.java +++ b/org.adempiere.base/src/org/idempiere/model/MappedModelFactory.java @@ -24,15 +24,30 @@ **********************************************************************/ package org.idempiere.model; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.sql.ResultSet; +import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; import java.util.function.Supplier; +import java.util.logging.Level; import org.adempiere.base.IModelFactory; +import org.adempiere.base.Model; import org.compiere.model.PO; +import org.compiere.util.CLogger; +import org.compiere.util.Env; +import org.osgi.framework.BundleContext; +import org.osgi.framework.wiring.BundleWiring; import org.osgi.service.component.annotations.Component; +import io.github.classgraph.AnnotationInfo; +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ClassInfoList; +import io.github.classgraph.ScanResult; + /** * @author hengsin * @@ -47,6 +62,8 @@ public class MappedModelFactory implements IModelFactory, IMappedModelFactory { private final ConcurrentHashMap> recordIdMap = new ConcurrentHashMap<>(); private final ConcurrentHashMap> resultSetMap = new ConcurrentHashMap<>(); + private static final CLogger s_log = CLogger.getCLogger(MappedModelFactory.class); + /** * default constructor */ @@ -85,4 +102,93 @@ public class MappedModelFactory implements IModelFactory, IMappedModelFactory { recordIdMap.remove(tableName); resultSetMap.remove(tableName); } + + @Override + public void scan(BundleContext context, String... packages) { + ClassLoader classLoader = context.getBundle().adapt(BundleWiring.class).getClassLoader(); + + ClassGraph graph = new ClassGraph() + .enableAnnotationInfo() + .overrideClassLoaders(classLoader) + .disableNestedJarScanning() + .disableModuleScanning() + .acceptPackagesNonRecursive(packages); + + try (ScanResult scanResult = graph.scan()) + { + + for (ClassInfo classInfo : scanResult.getClassesWithAnnotation(Model.class)) { + if (classInfo.isAbstract()) + continue; + String className = classInfo.getName(); + AnnotationInfo annotationInfo = classInfo.getAnnotationInfo(Model.class); + String tableName = (String) annotationInfo.getParameterValues().getValue("table"); + + // find subclass (if any) + ClassInfoList subclasses = classInfo.getSubclasses().directOnly(); + while(!subclasses.isEmpty()) { + className = subclasses.get(0).getName(); + subclasses = subclasses.get(0).getSubclasses().directOnly(); + } + + try { + final Class clazz = classLoader.loadClass(className); + Supplier> classSupplier = () -> { return clazz; }; + Constructor idConstructor = clazz.getDeclaredConstructor(new Class[]{Properties.class, int.class, String.class}); + RecordIdFunction recordIdFunction = new RecordIdFunction(idConstructor); + Constructor rsConstructor = clazz.getDeclaredConstructor(new Class[]{Properties.class, ResultSet.class, String.class}); + ResultSetFunction resultSetFunction = new ResultSetFunction(rsConstructor); + addMapping(tableName, classSupplier, recordIdFunction, resultSetFunction); + } catch (Exception e) { + if (s_log.isLoggable(Level.INFO)) + s_log.log(Level.INFO, e.getMessage(), e); + } + } + } + } + + private static final class RecordIdFunction implements BiFunction { + private Constructor constructor; + + private RecordIdFunction(Constructor constructor) { + this.constructor = constructor; + } + + @Override + public PO apply(Integer id, String trxName) { + if (constructor != null) { + try { + return (PO) constructor.newInstance(Env.getCtx(), id, trxName); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + constructor = null; + throw new RuntimeException(e); + } + } + return null; + } + } + + private static final class ResultSetFunction implements BiFunction { + private Constructor constructor; + + private ResultSetFunction(Constructor constructor) { + this.constructor = constructor; + } + + @Override + public PO apply(ResultSet rs, String trxName) { + if (constructor != null) { + try { + return (PO)constructor.newInstance(new Object[] {Env.getCtx(), rs, trxName}); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + constructor = null; + throw new RuntimeException(e); + } + } + return null; + } + + } } diff --git a/org.idempiere.test/src/org/idempiere/test/model/MappedModelFactoryTest.java b/org.idempiere.test/src/org/idempiere/test/model/MappedModelFactoryTest.java index 7541d299c6..2f7a61e01d 100644 --- a/org.idempiere.test/src/org/idempiere/test/model/MappedModelFactoryTest.java +++ b/org.idempiere.test/src/org/idempiere/test/model/MappedModelFactoryTest.java @@ -33,6 +33,7 @@ import java.util.Properties; import org.adempiere.base.Core; import org.adempiere.base.IModelFactory; +import org.compiere.model.MColor; import org.compiere.model.MTable; import org.compiere.model.PO; import org.compiere.model.X_Test; @@ -42,6 +43,7 @@ import org.idempiere.model.IMappedModelFactory; import org.idempiere.model.MappedModelFactory; import org.idempiere.test.AbstractTestCase; import org.idempiere.test.TestActivator; +import org.idempiere.test.model.annotated.MyAnnotatedColorModel; import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -83,6 +85,16 @@ public class MappedModelFactoryTest extends AbstractTestCase { assertTrue(po instanceof MyTest2, "PO not instanceof MyTest2. PO.className="+po.getClass().getName()); } + @Test + @Order(3) + public void testAnnotatedModelMapping() { + BundleContext bc = TestActivator.context; + Core.getMappedModelFactory().scan(bc, "org.idempiere.test.model.annotated"); + CacheMgt.get().reset(); + PO po = MTable.get(MColor.Table_ID).getPO(0, getTrxName()); + assertTrue(po instanceof MyAnnotatedColorModel, "PO not instanceof MyAnnotatedColorModel. PO.className="+po.getClass().getName()); + } + private final static class MyFactory extends MappedModelFactory { public MyFactory() { diff --git a/org.idempiere.test/src/org/idempiere/test/model/annotated/MyAnnotatedColorModel.java b/org.idempiere.test/src/org/idempiere/test/model/annotated/MyAnnotatedColorModel.java new file mode 100644 index 0000000000..57cc7d8814 --- /dev/null +++ b/org.idempiere.test/src/org/idempiere/test/model/annotated/MyAnnotatedColorModel.java @@ -0,0 +1,52 @@ +/*********************************************************************** + * 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.idempiere.test.model.annotated; + +import java.sql.ResultSet; +import java.util.Properties; + +import org.compiere.model.MColor; + +/** + * + * @author hengsin + * + */ +@org.adempiere.base.Model(table = MColor.Table_Name) +public class MyAnnotatedColorModel extends MColor { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public MyAnnotatedColorModel(Properties ctx, int Test_ID, String trxName) { + super(ctx, Test_ID, trxName); + } + + public MyAnnotatedColorModel(Properties ctx, ResultSet rs, String trxName) { + super(ctx, rs, trxName); + } +}