diff --git a/org.adempiere.base/OSGI-INF/addressvalidationeventhandler.xml b/org.adempiere.base/OSGI-INF/addressvalidationeventhandler.xml
deleted file mode 100644
index cb394594fe..0000000000
--- a/org.adempiere.base/OSGI-INF/addressvalidationeventhandler.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/org.adempiere.base/OSGI-INF/org.adempiere.base.DefaultAnnotationBasedEventManager.xml b/org.adempiere.base/OSGI-INF/org.adempiere.base.DefaultAnnotationBasedEventManager.xml
new file mode 100644
index 0000000000..e5b18b745e
--- /dev/null
+++ b/org.adempiere.base/OSGI-INF/org.adempiere.base.DefaultAnnotationBasedEventManager.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/org.adempiere.base/src/org/adempiere/base/AnnotationBasedEventManager.java b/org.adempiere.base/src/org/adempiere/base/AnnotationBasedEventManager.java
new file mode 100644
index 0000000000..82f25b39b0
--- /dev/null
+++ b/org.adempiere.base/src/org/adempiere/base/AnnotationBasedEventManager.java
@@ -0,0 +1,372 @@
+/***********************************************************************
+ * 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.List;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.logging.Level;
+
+import org.adempiere.base.annotation.EventTopicDelegate;
+import org.adempiere.base.annotation.ImportEventTopic;
+import org.adempiere.base.annotation.ModelEventTopic;
+import org.adempiere.base.annotation.ProcessEventTopic;
+import org.adempiere.base.event.IEventManager;
+import org.adempiere.base.event.annotations.EventDelegate;
+import org.adempiere.base.event.annotations.ModelEventDelegate;
+import org.adempiere.base.event.annotations.ModelEventHandler;
+import org.adempiere.base.event.annotations.SimpleEventHandler;
+import org.adempiere.base.event.annotations.imp.ImportEventDelegate;
+import org.adempiere.base.event.annotations.imp.ImportEventHandler;
+import org.adempiere.base.event.annotations.process.ProcessEventDelegate;
+import org.adempiere.base.event.annotations.process.ProcessEventHandler;
+import org.compiere.model.PO;
+import org.compiere.util.CLogger;
+import org.compiere.util.Util;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+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.Deactivate;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventHandler;
+import org.osgi.util.tracker.ServiceTracker;
+import org.osgi.util.tracker.ServiceTrackerCustomizer;
+
+import io.github.classgraph.AnnotationClassRef;
+import io.github.classgraph.AnnotationInfo;
+import io.github.classgraph.ClassGraph;
+import io.github.classgraph.ClassInfo;
+import io.github.classgraph.ScanResult;
+
+/**
+ * Scan, discover and register classes with {@link EventTopicDelegate} annotation
+ * @author hengsin
+ *
+ */
+public abstract class AnnotationBasedEventManager {
+
+ private static final CLogger s_log = CLogger.getCLogger(AnnotationBasedEventManager.class);
+
+ private IEventManager eventManager;
+ private BundleContext bundleContext;
+ private List handlers = new ArrayList<>();
+
+ private ServiceTracker serviceTracker;
+
+ /**
+ * default constructor
+ */
+ public AnnotationBasedEventManager() {
+ }
+
+ /**
+ * Subclass would override this to define the list of packages to perform the scan, discover and register operation
+ * @return packages to scan
+ */
+ public abstract String[] getPackages();
+
+ @Activate
+ public void activate(ComponentContext context) {
+ bundleContext = context.getBundleContext();
+ serviceTracker = new ServiceTracker(bundleContext, IEventManager.class,
+ new Customizer());
+ serviceTracker.open();
+ }
+
+ @Deactivate
+ public void deactivate(ComponentContext context) {
+ if (serviceTracker != null) {
+ serviceTracker.close();
+ serviceTracker = null;
+ }
+ if (eventManager != null) {
+ if (handlers.size() > 0)
+ unbindService(eventManager);
+ eventManager = null;
+ }
+ }
+
+ /**
+ *
+ * @param eventManager
+ */
+ protected void bindService(IEventManager eventManager) {
+ this.eventManager = eventManager;
+ if (eventManager != null)
+ scan();
+ }
+
+ /**
+ *
+ * @param eventManager
+ */
+ protected void unbindService(IEventManager eventManager) {
+ if (eventManager != null && eventManager == this.eventManager) {
+ if (handlers.size() > 0) {
+ for(EventHandler handler : handlers)
+ eventManager.unregister(handler);
+ }
+ this.eventManager = null;
+ }
+ }
+
+ /**
+ * Perform scan, discover and register of annotated classes
+ */
+ protected void scan() {
+ long start = System.currentTimeMillis();
+ ClassLoader classLoader = bundleContext.getBundle().adapt(BundleWiring.class).getClassLoader();
+
+ ClassGraph graph = new ClassGraph()
+ .enableAnnotationInfo()
+ .overrideClassLoaders(classLoader)
+ .disableNestedJarScanning()
+ .disableModuleScanning()
+ .acceptPackagesNonRecursive(getPackages());
+
+ try (ScanResult scanResult = graph.scan())
+ {
+ for (ClassInfo classInfo : scanResult.getClassesWithAnnotation(EventTopicDelegate.class)) {
+ if (classInfo.isAbstract())
+ continue;
+ String className = classInfo.getName();
+ AnnotationInfo baseInfo = classInfo.getAnnotationInfo(EventTopicDelegate.class);
+ String filter = (String) baseInfo.getParameterValues().getValue("filter");
+ if (classInfo.hasAnnotation(ModelEventTopic.class)) {
+ AnnotationInfo annotationInfo = classInfo.getAnnotationInfo(ModelEventTopic.class);
+ modelEventDelegate(classLoader, className, annotationInfo, filter);
+ } else if (classInfo.hasAnnotation(ImportEventTopic.class)) {
+ AnnotationInfo annotationInfo = classInfo.getAnnotationInfo(ImportEventTopic.class);
+ importEventDelegate(classLoader, className, annotationInfo, filter);
+ } else if (classInfo.hasAnnotation(ProcessEventTopic.class)) {
+ AnnotationInfo annotationInfo = classInfo.getAnnotationInfo(ProcessEventTopic.class);
+ processEventDelegate(classLoader, className, annotationInfo, filter);
+ } else {
+ simpleEventDelegate(classLoader, className, filter);
+ }
+ }
+ }
+ long end = System.currentTimeMillis();
+ if (s_log.isLoggable(Level.INFO))
+ s_log.info(this.getClass().getSimpleName() + " loaded "+handlers.size() +" classes in "
+ +((end-start)/1000f) + "s");
+
+ }
+
+ private void simpleEventDelegate(ClassLoader classLoader, String className, String filter) {
+ try {
+ @SuppressWarnings("unchecked")
+ Class extends EventDelegate> delegateClass = (Class extends EventDelegate>) classLoader.loadClass(className);
+ Constructor> constructor = delegateClass.getConstructor(Event.class);
+ EventDelegateSupplier supplier = new EventDelegateSupplier(constructor);
+ SimpleEventHandler handler = new SimpleEventHandler(delegateClass, supplier);
+ if (!Util.isEmpty(filter, true))
+ handler.setFilter(filter);
+ handlers.add(handler);
+ eventManager.register(handler.getTopics(), handler.getFilter(), handler);
+ } catch (Exception e) {
+ if (s_log.isLoggable(Level.INFO))
+ s_log.log(Level.INFO, e.getMessage(), e);
+ }
+ }
+
+ private void processEventDelegate(ClassLoader classLoader, String className, AnnotationInfo annotationInfo, String filter) {
+ try {
+ String processUUID = (String) annotationInfo.getParameterValues().getValue("processUUID");
+ @SuppressWarnings("unchecked")
+ Class extends ProcessEventDelegate> delegateClass = (Class extends ProcessEventDelegate>) classLoader.loadClass(className);
+ Constructor> constructor = delegateClass.getConstructor(Event.class);
+ ProcessDelegateSupplier supplier = new ProcessDelegateSupplier(constructor);
+ ProcessEventHandler handler = new ProcessEventHandler(delegateClass, processUUID, supplier);
+ if (!Util.isEmpty(filter, true))
+ handler.setFilter(filter);
+ handlers.add(handler);
+ eventManager.register(handler.getTopics(), handler.getFilter(), handler);
+ } catch (Exception e) {
+ if (s_log.isLoggable(Level.INFO))
+ s_log.log(Level.INFO, e.getMessage(), e);
+ }
+ }
+
+ private void importEventDelegate(ClassLoader classLoader, String className, AnnotationInfo annotationInfo, String filter) {
+ try {
+ String importTableName = (String) annotationInfo.getParameterValues().getValue("importTableName");
+ @SuppressWarnings("unchecked")
+ Class extends ImportEventDelegate> delegateClass = (Class extends ImportEventDelegate>) classLoader.loadClass(className);
+ Constructor> constructor = delegateClass.getConstructor(Event.class);
+ ImportDelegateSupplier supplier = new ImportDelegateSupplier(constructor);
+ ImportEventHandler handler = new ImportEventHandler(delegateClass, importTableName, supplier);
+ if (!Util.isEmpty(filter, true))
+ handler.setFilter(filter);
+ handlers.add(handler);
+ eventManager.register(handler.getTopics(), handler.getFilter(), handler);
+ } catch (Exception e) {
+ if (s_log.isLoggable(Level.INFO))
+ s_log.log(Level.INFO, e.getMessage(), e);
+ }
+ }
+
+ private void modelEventDelegate(ClassLoader classLoader, String className, AnnotationInfo annotationInfo, String filter) {
+ try {
+ AnnotationClassRef classRef = (AnnotationClassRef) annotationInfo.getParameterValues().getValue("modelClass");
+ @SuppressWarnings("unchecked")
+ Class extends PO> modelClass = (Class extends PO>)classRef.loadClass();
+ @SuppressWarnings("unchecked")
+ Class extends ModelEventDelegate extends PO>> delegateClass = (Class>) classLoader.loadClass(className);
+ Constructor> constructor = delegateClass.getDeclaredConstructor(modelClass, Event.class);
+ ModelDelegateSupplier supplier = new ModelDelegateSupplier(constructor);
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ ModelEventHandler> handler = new ModelEventHandler(modelClass, delegateClass, supplier);
+ if (!Util.isEmpty(filter, true))
+ handler.setFilter(filter);
+ handlers.add(handler);
+ eventManager.register(handler.getTopics(), handler.getFilter(), handler);
+ } catch (Exception e) {
+ if (s_log.isLoggable(Level.INFO))
+ s_log.log(Level.INFO, e.getMessage(), e);
+ }
+ }
+
+ private class Customizer implements ServiceTrackerCustomizer {
+ @Override
+ public IEventManager addingService(ServiceReference reference) {
+ IEventManager eventManager = bundleContext.getService(reference);
+ bindService(eventManager);
+ return eventManager;
+ }
+
+ @Override
+ public void modifiedService(ServiceReference reference, IEventManager service) {
+ if (eventManager != null && eventManager != service) {
+ unbindService(eventManager);
+ bindService(service);
+ }
+ }
+
+ @Override
+ public void removedService(ServiceReference reference, IEventManager service) {
+ unbindService(service);
+ }
+ }
+
+ private static class ModelDelegateSupplier implements BiFunction> {
+
+ private Constructor> constructor;
+
+ private ModelDelegateSupplier(Constructor> constructor) {
+ this.constructor = constructor;
+ }
+
+ @Override
+ public ModelEventDelegate> apply(PO t, Event u) {
+ ModelEventDelegate> delegate = null;
+ if (constructor != null) {
+ try {
+ delegate = (ModelEventDelegate>) constructor.newInstance(t, u);
+ } catch (Exception e) {
+ constructor = null;
+ s_log.log(Level.WARNING, e.getMessage(), e);
+ }
+ }
+ return delegate;
+ }
+ }
+
+ private static class ImportDelegateSupplier implements Function {
+
+ private Constructor> constructor;
+
+ private ImportDelegateSupplier(Constructor> constructor) {
+ this.constructor = constructor;
+ }
+
+ @Override
+ public ImportEventDelegate apply(Event t) {
+ ImportEventDelegate delegate = null;
+ if (constructor != null) {
+ try {
+ delegate = (ImportEventDelegate) constructor.newInstance(t);
+ } catch (Exception e) {
+ constructor = null;
+ s_log.log(Level.WARNING, e.getMessage(), e);
+ }
+ }
+ return delegate;
+ }
+
+ }
+
+ private static class ProcessDelegateSupplier implements Function {
+
+ private Constructor> constructor;
+
+ private ProcessDelegateSupplier(Constructor> constructor) {
+ this.constructor = constructor;
+ }
+
+ @Override
+ public ProcessEventDelegate apply(Event t) {
+ ProcessEventDelegate delegate = null;
+ if (constructor != null) {
+ try {
+ delegate = (ProcessEventDelegate) constructor.newInstance(t);
+ } catch (Exception e) {
+ constructor = null;
+ s_log.log(Level.WARNING, e.getMessage(), e);
+ }
+ }
+ return delegate;
+ }
+
+ }
+
+ private static class EventDelegateSupplier implements Function {
+
+ private Constructor> constructor;
+
+ private EventDelegateSupplier(Constructor> constructor) {
+ this.constructor = constructor;
+ }
+
+ @Override
+ public EventDelegate apply(Event t) {
+ EventDelegate delegate = null;
+ if (constructor != null) {
+ try {
+ delegate = (EventDelegate) constructor.newInstance(t);
+ } catch (Exception e) {
+ constructor = null;
+ s_log.log(Level.WARNING, e.getMessage(), e);
+ }
+ }
+ return delegate;
+ }
+
+ }
+}
diff --git a/org.adempiere.base/src/org/adempiere/base/DefaultAnnotationBasedEventManager.java b/org.adempiere.base/src/org/adempiere/base/DefaultAnnotationBasedEventManager.java
new file mode 100644
index 0000000000..369a75956e
--- /dev/null
+++ b/org.adempiere.base/src/org/adempiere/base/DefaultAnnotationBasedEventManager.java
@@ -0,0 +1,43 @@
+/***********************************************************************
+ * 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 org.osgi.service.component.annotations.Component;
+
+@Component(immediate = true, service = {})
+public class DefaultAnnotationBasedEventManager extends AnnotationBasedEventManager {
+
+ /**
+ * default constructor
+ */
+ public DefaultAnnotationBasedEventManager() {
+ }
+
+ @Override
+ public String[] getPackages() {
+ return new String[] {"org.adempiere.base.event.delegate"};
+ }
+
+}
diff --git a/org.adempiere.base/src/org/adempiere/base/annotation/EventTopicDelegate.java b/org.adempiere.base/src/org/adempiere/base/annotation/EventTopicDelegate.java
new file mode 100644
index 0000000000..667a0010be
--- /dev/null
+++ b/org.adempiere.base/src/org/adempiere/base/annotation/EventTopicDelegate.java
@@ -0,0 +1,50 @@
+/***********************************************************************
+ * 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.annotation;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.adempiere.base.event.annotations.EventDelegate;
+
+/**
+ *
+ * Annotation for OSGi Event Topic Delegate.
+ * Works with {@link EventDelegate} and its sub classes
+ * @author hengsin
+ *
+ */
+@Retention(RUNTIME)
+@Target(ElementType.TYPE)
+public @interface EventTopicDelegate {
+ /**
+ * Optional event topic filter
+ * @return filter
+ */
+ String filter() default "";
+}
diff --git a/org.adempiere.base/src/org/adempiere/base/annotation/ImportEventTopic.java b/org.adempiere.base/src/org/adempiere/base/annotation/ImportEventTopic.java
new file mode 100644
index 0000000000..8df3d89ee1
--- /dev/null
+++ b/org.adempiere.base/src/org/adempiere/base/annotation/ImportEventTopic.java
@@ -0,0 +1,49 @@
+/***********************************************************************
+ * 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.annotation;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.adempiere.base.event.annotations.imp.ImportEventDelegate;
+
+/**
+ * Define parameter for {@link ImportEventDelegate}
+ * Works with classes with {@link EventTopicDelegate} annotation
+ * @author hengsin
+ *
+ */
+@Retention(RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ImportEventTopic {
+ /**
+ * Import table (I_*) name for {@link ImportEventDelegate}
+ * @return import table name
+ */
+ String importTableName();
+}
diff --git a/org.adempiere.base/src/org/adempiere/base/annotation/ModelEventTopic.java b/org.adempiere.base/src/org/adempiere/base/annotation/ModelEventTopic.java
new file mode 100644
index 0000000000..2c0cdb4176
--- /dev/null
+++ b/org.adempiere.base/src/org/adempiere/base/annotation/ModelEventTopic.java
@@ -0,0 +1,50 @@
+/***********************************************************************
+ * 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.annotation;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.adempiere.base.event.annotations.ModelEventDelegate;
+import org.compiere.model.PO;
+
+/**
+ * Specify parameter for {@link ModelEventDelegate}
+ * Works with classes with {@link EventTopicDelegate} annotation
+ * @author hengsin
+ *
+ */
+@Retention(RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ModelEventTopic {
+ /**
+ * Model class (M* or X_*) for {@link ModelEventDelegate}
+ * @return model class
+ */
+ Class extends PO> modelClass();
+}
diff --git a/org.adempiere.base/src/org/adempiere/base/annotation/ProcessEventTopic.java b/org.adempiere.base/src/org/adempiere/base/annotation/ProcessEventTopic.java
new file mode 100644
index 0000000000..aeac806a63
--- /dev/null
+++ b/org.adempiere.base/src/org/adempiere/base/annotation/ProcessEventTopic.java
@@ -0,0 +1,49 @@
+/***********************************************************************
+ * 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.annotation;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.adempiere.base.event.annotations.process.ProcessEventDelegate;
+
+/**
+ * Define parameter for {@link ProcessEventDelegate}
+ * Works with classes with {@link EventTopicDelegate} annotation
+ * @author hengsin
+ *
+ */
+@Retention(RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ProcessEventTopic {
+ /**
+ * AD_Process.AD_Process_UU (uuid) value for {@link ProcessEventDelegate}
+ * @return process uuid
+ */
+ String processUUID();
+}
diff --git a/org.adempiere.base/src/org/adempiere/base/event/AddressValidationEventHandler.java b/org.adempiere.base/src/org/adempiere/base/event/AddressValidationEventHandler.java
deleted file mode 100644
index 7b8fb7da16..0000000000
--- a/org.adempiere.base/src/org/adempiere/base/event/AddressValidationEventHandler.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/******************************************************************************
- * Copyright (C) 2013 Elaine Tan *
- * Copyright (C) 2013 Trek Global
- * 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.event;
-
-import java.util.StringTokenizer;
-
-import org.compiere.model.I_C_Location;
-import org.compiere.model.MAddressValidation;
-import org.compiere.model.MLocation;
-import org.compiere.model.MSysConfig;
-import org.compiere.model.PO;
-import org.osgi.service.event.Event;
-
-/**
- * Address validation event handler
- * @author Elaine
- *
- */
-public class AddressValidationEventHandler extends AbstractEventHandler {
-
- @Override
- protected void doHandleEvent(Event event) {
- String topic = event.getTopic();
- if (topic.equals(IEventTopics.PO_BEFORE_NEW) || topic.equals(IEventTopics.PO_BEFORE_CHANGE))
- {
- PO po = getPO(event);
- if (po.get_TableName().equals(I_C_Location.Table_Name))
- {
- MLocation location = (MLocation) po;
-
- String addressValidation = MSysConfig.getValue(MSysConfig.ADDRESS_VALIDATION, null, location.getAD_Client_ID());
- boolean isEnabled = false;
- if (addressValidation != null && addressValidation.trim().length() > 0 && location.getCountry() != null)
- {
- StringTokenizer st = new StringTokenizer(addressValidation, ";");
- while (st.hasMoreTokens())
- {
- String token = st.nextToken().trim();
- if (token.equals(location.getCountry().getCountryCode().trim()))
- {
- isEnabled = true;
- break;
- }
- }
- }
-
- if (!isEnabled)
- return;
-
- MAddressValidation validation = null;
- if (location.getC_AddressValidation_ID() > 0)
- validation = new MAddressValidation(location.getCtx(), location.getC_AddressValidation_ID(), location.get_TrxName());
- if (validation == null)
- validation = MAddressValidation.getDefaultAddressValidation(location.getCtx(), location.getAD_Client_ID(), location.get_TrxName());
- if (validation != null)
- location.processOnline(validation.getC_AddressValidation_ID());
- }
- }
- }
-
- @Override
- protected void initialize() {
- registerTableEvent(IEventTopics.PO_BEFORE_NEW, I_C_Location.Table_Name);
- registerTableEvent(IEventTopics.PO_BEFORE_CHANGE, I_C_Location.Table_Name);
- }
-
-}
\ No newline at end of file
diff --git a/org.adempiere.base/src/org/adempiere/base/event/annotations/EventDelegate.java b/org.adempiere.base/src/org/adempiere/base/event/annotations/EventDelegate.java
index f0b515d673..3151bbb21a 100644
--- a/org.adempiere.base/src/org/adempiere/base/event/annotations/EventDelegate.java
+++ b/org.adempiere.base/src/org/adempiere/base/event/annotations/EventDelegate.java
@@ -30,6 +30,7 @@ import org.osgi.service.event.Event;
*
* Annotation driven event delegate base class that works together with {@link BaseEventHandler}.
* Subclass implementation doesn't have to be thread safe as event delegate is create and throw away for each event call.
+ * Subclass should use {@link EventTopic} or one of its derived annotation to define the event topic to handle
* @author hengsin
*
*/
diff --git a/org.adempiere.base/src/org/adempiere/base/event/delegate/AddressValidationEventDelegate.java b/org.adempiere.base/src/org/adempiere/base/event/delegate/AddressValidationEventDelegate.java
new file mode 100644
index 0000000000..90d29e24c8
--- /dev/null
+++ b/org.adempiere.base/src/org/adempiere/base/event/delegate/AddressValidationEventDelegate.java
@@ -0,0 +1,98 @@
+/***********************************************************************
+ * 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.event.delegate;
+
+import java.util.StringTokenizer;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.adempiere.base.annotation.EventTopicDelegate;
+import org.adempiere.base.annotation.ModelEventTopic;
+import org.adempiere.base.event.annotations.ModelEventDelegate;
+import org.adempiere.base.event.annotations.po.BeforeChange;
+import org.adempiere.base.event.annotations.po.BeforeNew;
+import org.compiere.model.MAddressValidation;
+import org.compiere.model.MLocation;
+import org.compiere.model.MSysConfig;
+import org.osgi.service.event.Event;
+
+/**
+ *
+ * @author hengsin
+ *
+ */
+@EventTopicDelegate
+@ModelEventTopic(modelClass = MLocation.class)
+public class AddressValidationEventDelegate extends ModelEventDelegate {
+
+ /**
+ *
+ * @param po
+ * @param event
+ */
+ public AddressValidationEventDelegate(MLocation po, Event event) {
+ super(po, event);
+ }
+
+ @BeforeNew
+ @BeforeChange
+ public void beforeCreateOrUpdate() {
+ MLocation location = getModel();
+
+ //for unit test checking (see org.idempiere.test.event.EventHandlerTest#testAddressValidationDelegate)
+ if (getEvent().containsProperty(getClass().getName())) {
+ Object value = getEvent().getProperty(getClass().getName());
+ if (value instanceof AtomicInteger) {
+ ((AtomicInteger) value).set(1);
+ }
+ }
+
+ String addressValidation = MSysConfig.getValue(MSysConfig.ADDRESS_VALIDATION, null, location.getAD_Client_ID());
+ boolean isEnabled = false;
+ if (addressValidation != null && addressValidation.trim().length() > 0 && location.getCountry() != null)
+ {
+ StringTokenizer st = new StringTokenizer(addressValidation, ";");
+ while (st.hasMoreTokens())
+ {
+ String token = st.nextToken().trim();
+ if (token.equals(location.getCountry().getCountryCode().trim()))
+ {
+ isEnabled = true;
+ break;
+ }
+ }
+ }
+
+ if (!isEnabled)
+ return;
+
+ MAddressValidation validation = null;
+ if (location.getC_AddressValidation_ID() > 0)
+ validation = new MAddressValidation(location.getCtx(), location.getC_AddressValidation_ID(), location.get_TrxName());
+ if (validation == null)
+ validation = MAddressValidation.getDefaultAddressValidation(location.getCtx(), location.getAD_Client_ID(), location.get_TrxName());
+ if (validation != null)
+ location.processOnline(validation.getC_AddressValidation_ID());
+ }
+}
diff --git a/org.idempiere.test/src/org/idempiere/test/event/EventHandlerTest.java b/org.idempiere.test/src/org/idempiere/test/event/EventHandlerTest.java
index e8a337b3f6..c3723a86a3 100644
--- a/org.idempiere.test/src/org/idempiere/test/event/EventHandlerTest.java
+++ b/org.idempiere.test/src/org/idempiere/test/event/EventHandlerTest.java
@@ -31,8 +31,11 @@ import static org.junit.jupiter.api.Assertions.fail;
import java.math.BigDecimal;
import java.sql.Timestamp;
+import java.util.concurrent.atomic.AtomicInteger;
import org.adempiere.base.Core;
+import org.adempiere.base.event.EventManager;
+import org.adempiere.base.event.EventProperty;
import org.adempiere.base.event.FactsEventData;
import org.adempiere.base.event.annotations.AfterLogin;
import org.adempiere.base.event.annotations.EventDelegate;
@@ -52,12 +55,16 @@ import org.adempiere.model.ImportValidator;
import org.compiere.model.MBPartner;
import org.compiere.model.MInOut;
import org.compiere.model.MInOutLine;
+import org.compiere.model.MLocation;
import org.compiere.model.MOrder;
import org.compiere.model.MOrderLine;
import org.compiere.model.MOrg;
import org.compiere.model.MProcess;
import org.compiere.model.MProduct;
+import org.compiere.model.MSysConfig;
import org.compiere.model.ModelValidationEngine;
+import org.compiere.model.ModelValidator;
+import org.compiere.model.PO;
import org.compiere.model.X_I_BPartner;
import org.compiere.model.X_I_Product;
import org.compiere.process.DocAction;
@@ -66,6 +73,7 @@ import org.compiere.process.ImportBPartner;
import org.compiere.process.ImportProduct;
import org.compiere.process.ProcessInfo;
import org.compiere.process.ServerProcessCtl;
+import org.compiere.util.CacheMgt;
import org.compiere.util.Env;
import org.compiere.util.KeyNamePair;
import org.compiere.util.Login;
@@ -315,6 +323,50 @@ public class EventHandlerTest extends AbstractTestCase {
"MyAfterImportDelegate not call. context="+Env.getContext(Env.getCtx(), MyAfterImportDelegate.class.getName()));
}
+ @Test
+ @Order(8)
+ public void testAddressValidationDelegate() {
+ int addressValidationSysConfigId = 200033;
+ String delegateName = "org.adempiere.base.event.delegate.AddressValidationEventDelegate";
+ MSysConfig sysconfig = new MSysConfig(Env.getCtx(), addressValidationSysConfigId, null);
+ String currentValue = sysconfig.getValue();
+ try {
+ try {
+ PO.setCrossTenantSafe();
+ sysconfig.setValue("US");
+ sysconfig.saveEx();
+ } finally {
+ PO.clearCrossTenantSafe();
+ }
+
+ CacheMgt.get().reset();
+
+ MLocation location = new MLocation(Env.getCtx(), 0, getTrxName());
+ location.setC_Country_ID(100);
+ AtomicInteger count = new AtomicInteger(0);
+ Event event = EventManager.newEvent(ModelValidator.tableEventTopics[ModelValidator.TYPE_BEFORE_NEW],
+ new EventProperty(EventManager.EVENT_DATA, location), new EventProperty(EventManager.TABLE_NAME_PROPERTY, location.get_TableName()),
+ new EventProperty(delegateName, count));
+ EventManager.getInstance().sendEvent(event);
+ assertTrue(count.get()==1, "AddressValidationEventDelegate not call for MLocation Before New Event");
+
+ count = new AtomicInteger(0);
+ event = EventManager.newEvent(ModelValidator.tableEventTopics[ModelValidator.TYPE_BEFORE_CHANGE],
+ new EventProperty(EventManager.EVENT_DATA, location), new EventProperty(EventManager.TABLE_NAME_PROPERTY, location.get_TableName()),
+ new EventProperty(delegateName, count));
+ EventManager.getInstance().sendEvent(event);
+ assertTrue(count.get()==1, "AddressValidationEventDelegate not call for MLocation Before Change Event");
+ } finally {
+ try {
+ PO.setCrossTenantSafe();
+ sysconfig.setValue(currentValue);
+ sysconfig.saveEx();
+ } finally {
+ PO.clearCrossTenantSafe();
+ }
+ }
+ }
+
private final static class MyBPBeforeNewDelegate extends ModelEventDelegate {
public MyBPBeforeNewDelegate(MBPartner bp, Event event) {