diff --git a/org.adempiere.base/OSGI-INF/org.adempiere.base.MappedColumnCalloutFactory.xml b/org.adempiere.base/OSGI-INF/org.adempiere.base.MappedColumnCalloutFactory.xml new file mode 100644 index 0000000000..339819c2c8 --- /dev/null +++ b/org.adempiere.base/OSGI-INF/org.adempiere.base.MappedColumnCalloutFactory.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/org.adempiere.base/src/org/adempiere/base/Core.java b/org.adempiere.base/src/org/adempiere/base/Core.java index 56517f4067..aae7cf8a49 100644 --- a/org.adempiere.base/src/org/adempiere/base/Core.java +++ b/org.adempiere.base/src/org/adempiere/base/Core.java @@ -57,6 +57,7 @@ import org.idempiere.fa.service.api.IDepreciationMethod; import org.idempiere.fa.service.api.IDepreciationMethodFactory; import org.idempiere.model.IMappedModelFactory; import org.idempiere.process.IMappedProcessFactory; +import org.osgi.framework.ServiceReference; /** * This is a facade class for the Service Locator. @@ -105,6 +106,7 @@ public class Core { } private static final CCache>> s_columnCalloutFactoryCache = new CCache<>(null, "List", 100, false); + private static final CCache>> s_columnCalloutFactoryNegativeCache = new CCache<>(null, "List Negative", 100, false); /** * @@ -116,49 +118,61 @@ public class Core { List list = new ArrayList(); String cacheKey = tableName + "." + columnName; - List> cache = s_columnCalloutFactoryCache.get(cacheKey); + List> cache = s_columnCalloutFactoryCache.get(cacheKey); + List> negativeCache = s_columnCalloutFactoryNegativeCache.get(cacheKey); + List> negativeServiceReferences = new ArrayList>(); + if (negativeCache != null) { + negativeServiceReferences.addAll(negativeCache); + } + List> cacheReferences = new ArrayList>(); + List> positiveReferenceHolders = new ArrayList<>(); if (cache != null) { - boolean staleReference = false; - for (IServiceReferenceHolder factory : cache) { - IColumnCalloutFactory service = factory.getService(); + for (IServiceReferenceHolder referenceHolder : cache) { + cacheReferences.add(referenceHolder.getServiceReference()); + IColumnCalloutFactory service = referenceHolder.getService(); if (service != null) { IColumnCallout[] callouts = service.getColumnCallouts(tableName, columnName); if (callouts != null && callouts.length > 0) { for(IColumnCallout callout : callouts) { list.add(callout); } - } else { - staleReference = true; - break; + positiveReferenceHolders.add(referenceHolder); + } else { + negativeServiceReferences.add(referenceHolder.getServiceReference()); } - } else { - staleReference = true; - break; } } - if (!staleReference) - return list; - else - s_columnCalloutFactoryCache.remove(cacheKey); } - List> factories = Service.locator().list(IColumnCalloutFactory.class).getServiceReferences(); - List> found = new ArrayList<>(); - if (factories != null) { - for(IServiceReferenceHolder factory : factories) { - IColumnCalloutFactory service = factory.getService(); + int positiveAdded = 0; + int negativeAdded = 0; + List> referenceHolders = Service.locator().list(IColumnCalloutFactory.class).getServiceReferences(); + if (referenceHolders != null) { + for(IServiceReferenceHolder referenceHolder : referenceHolders) { + if (cacheReferences.contains(referenceHolder.getServiceReference()) || negativeServiceReferences.contains(referenceHolder.getServiceReference())) + continue; + IColumnCalloutFactory service = referenceHolder.getService(); if (service != null) { IColumnCallout[] callouts = service.getColumnCallouts(tableName, columnName); if (callouts != null && callouts.length > 0) { for(IColumnCallout callout : callouts) { list.add(callout); } - found.add(factory); + positiveReferenceHolders.add(referenceHolder); + positiveAdded++; + } else { + negativeServiceReferences.add(referenceHolder.getServiceReference()); + negativeAdded++; } } - } - s_columnCalloutFactoryCache.put(cacheKey, found); + } } + + if (cache == null || cache.size() != positiveReferenceHolders.size() || positiveAdded > 0) + s_columnCalloutFactoryCache.put(cacheKey, positiveReferenceHolders); + if (negativeCache == null || negativeCache.size() != negativeServiceReferences.size() || negativeAdded > 0) + s_columnCalloutFactoryNegativeCache.put(cacheKey, negativeServiceReferences); + return list; } @@ -967,4 +981,25 @@ public class Core { } return processFactoryService; } + + private static IServiceReferenceHolder s_mappedColumnCalloutFactoryReference = null; + + /** + * + * @return {@link IMappedColumnCalloutFactory} + */ + public static IMappedColumnCalloutFactory getMappedColumnCalloutFactory() { + IMappedColumnCalloutFactory factoryService = null; + if (s_mappedColumnCalloutFactoryReference != null) { + factoryService = s_mappedColumnCalloutFactoryReference.getService(); + if (factoryService != null) + return factoryService; + } + IServiceReferenceHolder serviceReference = Service.locator().locate(IMappedColumnCalloutFactory.class).getServiceReference(); + if (serviceReference != null) { + factoryService = serviceReference.getService(); + s_mappedColumnCalloutFactoryReference = serviceReference; + } + return factoryService; + } } diff --git a/org.adempiere.base/src/org/adempiere/base/IMappedColumnCalloutFactory.java b/org.adempiere.base/src/org/adempiere/base/IMappedColumnCalloutFactory.java new file mode 100644 index 0000000000..a2246fe52c --- /dev/null +++ b/org.adempiere.base/src/org/adempiere/base/IMappedColumnCalloutFactory.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.adempiere.base; + +import java.util.function.Supplier; + +/** + * + * @author hengsin + * + */ +public interface IMappedColumnCalloutFactory { + + /** + * add mapping for callout + * @param tableName case insensitive table name or * to match all table + * @param columnName case insensitive column name or * to match all column + * @param supplier supplier for {@link IColumnCallout} instance + */ + void addMapping(String tableName, String columnName, Supplier supplier); + + /** + * remove mapping for callout + * @param tableName case insensitive table name or * to match all table + * @param columnName case insensitive column name or * to match all column + * @param supplier supplier for {@link IColumnCallout} instance + */ + void removeMapping(String tableName, String columnName, Supplier supplier); + +} \ No newline at end of file diff --git a/org.adempiere.base/src/org/adempiere/base/MappedColumnCalloutFactory.java b/org.adempiere.base/src/org/adempiere/base/MappedColumnCalloutFactory.java new file mode 100644 index 0000000000..5d52a60000 --- /dev/null +++ b/org.adempiere.base/src/org/adempiere/base/MappedColumnCalloutFactory.java @@ -0,0 +1,104 @@ +/*********************************************************************** + * 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.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.function.Supplier; + +import org.osgi.service.component.annotations.Component; + +/** + * + * @author hengsin + * + */ +@Component(name = "org.adempiere.base.MappedColumnCalloutFactory", + immediate = true, + service = {IColumnCalloutFactory.class, IMappedColumnCalloutFactory.class}, + property = {"service.ranking:Integer=1"}) +public class MappedColumnCalloutFactory implements IColumnCalloutFactory, IMappedColumnCalloutFactory { + + private final HashMap>> calloutMap = new HashMap<>(); + + /** + * default constructor + */ + public MappedColumnCalloutFactory() { + } + + @Override + public IColumnCallout[] getColumnCallouts(String tableName, String columnName) { + List calloutList = new ArrayList(); + StringBuilder key = new StringBuilder(); + key.append(tableName.toLowerCase()).append("|").append(columnName.toLowerCase()); + StringBuilder key1 = new StringBuilder(); + key1.append("*|").append(columnName.toLowerCase()); + StringBuilder key2 = new StringBuilder(); + key2.append(tableName.toLowerCase()).append("|*"); + synchronized (calloutMap) { + List> list = calloutMap.get(key.toString()); + if (list != null && list.size() > 0) { + list.forEach(e -> calloutList.add(e.get())); + } + list = calloutMap.get(key1.toString()); + if (list != null && list.size() > 0) { + list.forEach(e -> calloutList.add(e.get())); + } + list = calloutMap.get(key2.toString()); + if (list != null && list.size() > 0) { + list.forEach(e -> calloutList.add(e.get())); + } + } + return calloutList.isEmpty() ? null : calloutList.toArray(new IColumnCallout[0]); + } + + @Override + public void addMapping(String tableName, String columnName, Supplier supplier) { + StringBuilder key = new StringBuilder(); + key.append(tableName.toLowerCase()).append("|").append(columnName.toLowerCase()); + synchronized (calloutMap) { + List> list = calloutMap.get(key.toString()); + if (list == null) { + list = new ArrayList>(); + calloutMap.put(key.toString(), list); + } + list.add(supplier); + } + } + + @Override + public void removeMapping(String tableName, String columnName, Supplier supplier) { + StringBuilder key = new StringBuilder(); + key.append(tableName.toLowerCase()).append("|").append(columnName.toLowerCase()); + synchronized (calloutMap) { + List> list = calloutMap.get(key.toString()); + if (list != null) { + list.remove(supplier); + } + } + } +} diff --git a/org.idempiere.test/src/org/idempiere/test/model/MappedColumnCalloutFactoryTest.java b/org.idempiere.test/src/org/idempiere/test/model/MappedColumnCalloutFactoryTest.java new file mode 100644 index 0000000000..033ad87402 --- /dev/null +++ b/org.idempiere.test/src/org/idempiere/test/model/MappedColumnCalloutFactoryTest.java @@ -0,0 +1,107 @@ +/*********************************************************************** + * 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; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Properties; + +import org.adempiere.base.Core; +import org.adempiere.base.IColumnCallout; +import org.adempiere.base.IColumnCalloutFactory; +import org.adempiere.base.MappedColumnCalloutFactory; +import org.compiere.model.GridField; +import org.compiere.model.GridTab; +import org.compiere.model.MTest; +import org.idempiere.test.AbstractTestCase; +import org.idempiere.test.TestActivator; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.osgi.framework.BundleContext; + +/** + * @author hengsin + * + */ +@TestMethodOrder(OrderAnnotation.class) +public class MappedColumnCalloutFactoryTest extends AbstractTestCase { + + /** + * default constructor + */ + public MappedColumnCalloutFactoryTest() { + } + + @Test + @Order(1) + public void testDefaultMappedColumnCalloutFactory() { + var factory = Core.getMappedColumnCalloutFactory(); + factory.addMapping(MTest.Table_Name, MTest.COLUMNNAME_T_Amount, () -> new MyTestAmountCallout()); + + var list = Core.findCallout(MTest.Table_Name, MTest.COLUMNNAME_T_Amount); + var optional = list.stream().filter(e -> e instanceof MyTestAmountCallout).findFirst(); + assertTrue(optional.isPresent(), "Can't find MyTestAmountCallout column callout for " + MTest.Table_Name + "." + MTest.COLUMNNAME_T_Amount); + } + + @Test + @Order(2) + public void testCustomMappedColumnCalloutFactory() { + BundleContext bc = TestActivator.context; + Dictionary properties = new Hashtable(); + properties.put("service.ranking", Integer.valueOf(1)); + bc.registerService(IColumnCalloutFactory.class, new MyFactory(), properties); + var list = Core.findCallout(MTest.Table_Name, MTest.COLUMNNAME_T_Amount); + var optional = list.stream().filter(e -> e instanceof MyTestAmountCallout2).findFirst(); + assertTrue(optional.isPresent(), "Can't find MyTestAmountCallout2 column callout for " + MTest.Table_Name + "." + MTest.COLUMNNAME_T_Amount); + } + + private final static class MyFactory extends MappedColumnCalloutFactory { + public MyFactory() { + addMapping(MTest.Table_Name, MTest.COLUMNNAME_T_Amount, () -> new MyTestAmountCallout2()); + } + } + + private final static class MyTestAmountCallout implements IColumnCallout { + + @Override + public String start(Properties ctx, int WindowNo, GridTab mTab, GridField mField, Object value, Object oldValue) { + return null; + } + + } + + private final static class MyTestAmountCallout2 implements IColumnCallout { + + @Override + public String start(Properties ctx, int WindowNo, GridTab mTab, GridField mField, Object value, Object oldValue) { + return null; + } + + } +}