/****************************************************************************** * Product: Adempiere ERP & CRM Smart Business Solution * * Copyright (C) 1999-2006 ComPiere, Inc. All Rights Reserved. * * 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. * * For the text or an alternative of this public license, you may reach us * * ComPiere, Inc., 2620 Augustine Dr. #245, Santa Clara, CA 95054, USA * * or via info@compiere.org or http://www.compiere.org/license.html * *****************************************************************************/ package org.compiere.process; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.math.BigDecimal; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.TreeMap; import java.util.logging.Level; import java.util.stream.Collectors; import org.adempiere.base.annotation.Parameter; import org.adempiere.base.event.EventManager; import org.adempiere.base.event.EventProperty; import org.adempiere.base.event.IEventManager; import org.adempiere.base.event.IEventTopics; import org.adempiere.util.IProcessUI; import org.compiere.model.MPInstance; import org.compiere.model.PO; import org.compiere.util.CLogger; import org.compiere.util.DB; import org.compiere.util.Env; import org.compiere.util.Msg; import org.compiere.util.Trx; import org.compiere.util.TrxEventListener; import org.osgi.service.event.Event; /** * Abstract base class for Server Process. * * @author Jorg Janke * @version $Id: SvrProcess.java,v 1.4 2006/08/10 01:00:44 jjanke Exp $ * * @author Teo Sarca, SC ARHIPAC SERVICE SRL *
  • FR [ 1646891 ] SvrProcess - post process support *
  • BF [ 1877935 ] SvrProcess.process should catch all throwables *
  • FR [ 1877937 ] SvrProcess: added commitEx method *
  • BF [ 1878743 ] SvrProcess.getAD_User_ID *
  • BF [ 1935093 ] SvrProcess.unlock() is setting invalid result *
  • FR [ 2788006 ] SvrProcess: change access to some methods * https://sourceforge.net/p/adempiere/feature-requests/709/ */ @org.adempiere.base.annotation.Process public abstract class SvrProcess implements ProcessCall { /** Key to store process info as environment context attribute */ public static final String PROCESS_INFO_CTX_KEY = "ProcessInfo"; /** Key to store process UI as environment context attribute */ public static final String PROCESS_UI_CTX_KEY = "ProcessUI"; /** buffer log */ private List listEntryLog; /** * Add log to buffer, buffer is flush after commit of process transaction * @param id * @param date * @param number * @param msg * @param tableId * @param recordId */ public void addBufferLog(int id, Timestamp date, BigDecimal number, String msg, int tableId ,int recordId) { ProcessInfoLog entryLog = new ProcessInfoLog(id, date, number, msg, tableId, recordId); if (listEntryLog == null) listEntryLog = new ArrayList(); listEntryLog.add(entryLog); } /** * Server Process.
    * Note that the class is initiated by startProcess. */ public SvrProcess() { } // SvrProcess private Properties m_ctx; private ProcessInfo m_pi; /** Logger */ protected CLogger log = CLogger.getCLogger (getClass()); /** Is the Object locked */ private boolean m_locked = false; /** Loacked Object */ private PO m_lockedObject = null; /** Process Main transaction */ private Trx m_trx; protected IProcessUI processUI; /** Common Error Message */ protected static String MSG_SaveErrorRowNotFound = "@SaveErrorRowNotFound@"; protected static String MSG_InvalidArguments = "@InvalidArguments@"; /** * Start the process.
    * Calls the method process.
    * * @param ctx Context * @param pi Process Info * @return true if success * @see org.compiere.process.ProcessCall#startProcess(Properties, ProcessInfo, Trx) */ public final boolean startProcess (Properties ctx, ProcessInfo pi, Trx trx) { // Preparation m_ctx = ctx == null ? Env.getCtx() : ctx; m_pi = pi; m_trx = trx; //*** Trx boolean localTrx = m_trx == null; if (localTrx) { m_trx = Trx.get(Trx.createTrxName("SvrProcess"), true); m_trx.setDisplayName(getClass().getName()+"_startProcess"); } m_pi.setTransactionName(m_trx.getTrxName()); m_pi.setProcessUI(processUI); // ClassLoader contextLoader = Thread.currentThread().getContextClassLoader(); ClassLoader processLoader = getClass().getClassLoader(); try { if (processLoader != contextLoader) { Thread.currentThread().setContextClassLoader(processLoader); } lock(); boolean success = false; try { m_ctx.put(PROCESS_INFO_CTX_KEY, m_pi); if (processUI != null) m_ctx.put(PROCESS_UI_CTX_KEY, processUI); success = process(); } finally { m_ctx.remove(PROCESS_INFO_CTX_KEY); m_ctx.remove(PROCESS_UI_CTX_KEY); if (localTrx) { if (success) { try { m_trx.commit(true); } catch (Exception e) { log.log(Level.SEVERE, "Commit failed.", e); m_pi.addSummary("Commit Failed."); m_pi.setError(true); } } else m_trx.rollback(); m_trx.close(); m_trx = null; m_pi.setTransactionName(null); unlock(); // outside transaction processing [ teo_sarca, 1646891 ] postProcess(!m_pi.isError()); @SuppressWarnings("unused") Event eventPP = sendProcessEvent(IEventTopics.POST_PROCESS); } else { m_trx.addTrxEventListener(new TrxEventListener() { @Override public void afterRollback(Trx trx, boolean success) { } @Override public void afterCommit(Trx trx, boolean success) { } @Override public void afterClose(Trx trx) { unlock(); // outside transaction processing [ teo_sarca, 1646891 ] m_trx = null; postProcess(!m_pi.isError()); @SuppressWarnings("unused") Event eventPP = sendProcessEvent(IEventTopics.POST_PROCESS); } }); } Thread.currentThread().setContextClassLoader(contextLoader); } } finally { if (processLoader != contextLoader) { Thread.currentThread().setContextClassLoader(contextLoader); } } return !m_pi.isError(); } // startProcess /** * Execute Process * @return true if success */ private boolean process() { String msg = null; boolean success = true; try { autoFillParameters(); prepare(); // event before process Event eventBP = sendProcessEvent(IEventTopics.BEFORE_PROCESS); @SuppressWarnings("unchecked") List errorsBP = (List) eventBP.getProperty(IEventManager.EVENT_ERROR_MESSAGES); if (errorsBP != null && !errorsBP.isEmpty()) { msg = "@Error@" + errorsBP.get(0); } else { msg = doIt(); if (msg != null && ! msg.startsWith("@Error@")) { Event eventAP = sendProcessEvent(IEventTopics.AFTER_PROCESS); @SuppressWarnings("unchecked") List errorsAP = (List) eventAP.getProperty(IEventManager.EVENT_ERROR_MESSAGES); if (errorsAP != null && !errorsAP.isEmpty()) { msg = "@Error@" + errorsAP.get(0); } } } } catch (Throwable e) { msg = e.getLocalizedMessage(); if (msg == null) msg = e.toString(); if (e.getCause() != null) log.log(Level.SEVERE, Msg.parseTranslation(getCtx(), msg), e.getCause()); else log.log(Level.SEVERE, Msg.parseTranslation(getCtx(), msg), e); success = false; // throw new RuntimeException(e); } //transaction should rollback if there are error in process if(msg != null && msg.startsWith("@Error@")) success = false; if (success) { // if the connection has not been used, then the buffer log is never flushed // f.e. when the process uses local transactions like UUIDGenerator m_trx.getConnection(); m_trx.addTrxEventListener(new TrxEventListener() { @Override public void afterRollback(Trx trx, boolean success) { } @Override public void afterCommit(Trx trx, boolean success) { if (success) flushBufferLog(); } @Override public void afterClose(Trx trx) { } }); } // Parse Variables msg = Msg.parseTranslation(m_ctx, msg); m_pi.setSummary (msg, !success); return success; } // process /** * Send OSGi event * @param topic * @return event object */ private Event sendProcessEvent(String topic) { Event event = EventManager.newEvent(topic, new EventProperty(EventManager.EVENT_DATA, m_pi), new EventProperty(EventManager.PROCESS_UID_PROPERTY, m_pi.getAD_Process_UU()), new EventProperty(EventManager.CLASS_NAME_PROPERTY, m_pi.getClassName()), new EventProperty(EventManager.PROCESS_CLASS_NAME_PROPERTY, this.getClass().getName())); EventManager.getInstance().sendEvent(event); return event; } /** * Prepare process - e.g., get Parameters. *
    {@code
    		ProcessInfoParameter[] para = getParameter();
    		for (int i = 0; i < para.length; i++)
    		{
    			String name = para[i].getParameterName();
    			if (para[i].getParameter() == null)
    				;
    			else if (name.equals("A_Asset_Group_ID"))
    				p_A_Asset_Group_ID = para[i].getParameterAsInt();
    			else if (name.equals("GuaranteeDate"))
    				p_GuaranteeDate = (Timestamp)para[i].getParameter();
    			else if (name.equals("AttachAsset"))
    				p_AttachAsset = "Y".equals(para[i].getParameter());
    			else
    				log.log(Level.SEVERE, "Unknown Parameter: " + name);
    		}
    	 *  }
    * @see Parameter */ abstract protected void prepare(); /** * Process implementation class will override this method to execution process actions. * @return Message (variables are parsed) * @throws Exception if not successful e.g. * throw new AdempiereUserError ("@FillMandatory@ @C_BankAccount_ID@"); */ abstract protected String doIt() throws Exception; /** * Post process actions (outside trx).
    * Please note that at this point the transaction is committed so * you can't rollback.
    * This method is useful if you need to do some custom work after * the process committed the changes (e.g. open some windows). * * @param success true if the process was success * @since 3.1.4 */ protected void postProcess(boolean success) { } /** * Commit * @deprecated suggested to use commitEx instead */ @Deprecated protected void commit() { if (m_trx != null) m_trx.commit(); } // commit /** * Commit and throw exception if error * @throws SQLException on commit error */ protected void commitEx() throws SQLException { if (m_trx != null) m_trx.commit(true); } /** * Rollback transaction */ protected void rollback() { if (m_trx != null) m_trx.rollback(); } // rollback /** * Lock PO.
    * Needs to be explicitly called. Unlock is automatic. * @param po object * @return true if locked */ protected boolean lockObject (PO po) { // Unlock existing if (m_locked || m_lockedObject != null) unlockObject(); // Nothing to lock if (po == null) return false; m_lockedObject = po; m_locked = m_lockedObject.lock(); return m_locked; } // lockObject /** * Is an object Locked? * @return true if object locked */ protected boolean isLocked() { return m_locked; } // isLocked /** * Unlock PO.
    * Is automatically called at the end of process. * @return true if unlocked or if there was nothing to unlock */ protected boolean unlockObject() { boolean success = true; if (m_locked || m_lockedObject != null) { success = m_lockedObject.unlock(null); } m_locked = false; m_lockedObject = null; return success; } // unlock /** * Get Process Info * @return Process Info */ public ProcessInfo getProcessInfo() { return m_pi; } // getProcessInfo /** * Get Context * @return Properties */ public Properties getCtx() { return m_ctx; } // getCtx /** * Get Name/Title * @return Name */ protected String getName() { return m_pi.getTitle(); } // getName /** * Get Process Instance * @return Process Instance */ protected int getAD_PInstance_ID() { return m_pi.getAD_PInstance_ID(); } // getAD_PInstance_ID /** * Get AD_Table_ID * @return AD_Table_ID */ protected int getTable_ID() { return m_pi.getTable_ID(); } // getRecord_ID /** * Get Record_ID * @return Record_ID */ protected int getRecord_ID() { return m_pi.getRecord_ID(); } // getRecord_ID /** * Get Record_IDs * * @return Record_IDs */ protected List getRecord_IDs() { return m_pi.getRecord_IDs(); } // getRecord_IDs /** * Get Record_UU * @return Record_UU */ protected String getRecord_UU() { return m_pi.getRecord_UU(); } // getRecord_UU /** * Get Record_UUs * * @return Record_UUs */ protected List getRecord_UUs() { return m_pi.getRecord_UUs(); } // getRecord_UUs /** * Get AD_User_ID * @return AD_User_ID of Process owner or -1 if not found */ protected int getAD_User_ID() { if (m_pi.getAD_User_ID() == null || m_pi.getAD_Client_ID() == null) { String sql = "SELECT AD_User_ID, AD_Client_ID FROM AD_PInstance WHERE AD_PInstance_ID=?"; PreparedStatement pstmt = null; ResultSet rs = null; try { pstmt = DB.prepareStatement(sql, get_TrxName()); pstmt.setInt(1, m_pi.getAD_PInstance_ID()); rs = pstmt.executeQuery(); if (rs.next()) { m_pi.setAD_User_ID (rs.getInt (1)); m_pi.setAD_Client_ID (rs.getInt(2)); } } catch (SQLException e) { log.log(Level.SEVERE, sql, e); } finally { DB.close(rs, pstmt); rs = null; pstmt = null; } } if (m_pi.getAD_User_ID() == null) return -1; return m_pi.getAD_User_ID().intValue(); } // getAD_User_ID /** * Get AD_Client_ID of process info * @return AD_Client_ID */ protected int getAD_Client_ID() { if (m_pi.getAD_Client_ID() == null) { getAD_User_ID(); // sets also Client if (m_pi.getAD_Client_ID() == null) return 0; } return m_pi.getAD_Client_ID().intValue(); } // getAD_Client_ID /** * Get Parameters * @return parameters */ protected ProcessInfoParameter[] getParameter() { ProcessInfoParameter[] retValue = m_pi.getParameter(); if (retValue == null) { ProcessInfoUtil.setParameterFromDB(m_pi); retValue = m_pi.getParameter(); } return retValue; } // getParameter /** * Add Log Entry with table name * @param id ID parameter, usually same as record id * @param date * @param number * @param msg * @param tableId * @param recordId */ public void addLog (int id, Timestamp date, BigDecimal number, String msg, int tableId ,int recordId) { if (m_pi != null) m_pi.addLog(id, date, number, msg,tableId,recordId); if (log.isLoggable(Level.INFO)) log.info(id + " - " + date + " - " + number + " - " + msg + " - " + tableId + " - " + recordId); } // addLog /** * Add Log Entry * @param id ID parameter, usually same as record id * @param date date or null * @param number number or null * @param msg message or null */ public void addLog (int id, Timestamp date, BigDecimal number, String msg) { if (m_pi != null) m_pi.addLog(id, date, number, msg); if (log.isLoggable(Level.INFO)) log.info(id + " - " + date + " - " + number + " - " + msg); } // addLog /** * Add Log * @param msg message */ public void addLog (String msg) { if (msg != null) addLog (0, null, null, msg); } // addLog /** * Flush buffer log to process info */ private void flushBufferLog () { if (listEntryLog == null) return; for (ProcessInfoLog entryLog : listEntryLog) { if (m_pi != null) m_pi.addLog(entryLog); if (log.isLoggable(Level.INFO)) log.info(entryLog.getP_ID() + " - " + entryLog.getP_Date() + " - " + entryLog.getP_Number() + " - " + entryLog.getP_Msg() + " - " + entryLog.getAD_Table_ID() + " - " + entryLog.getRecord_ID()); } listEntryLog = null; // flushed - to avoid flushing it again in case is called } /** * Save Progress Log Entry to DB immediately * @param date date or null * @param id record id or 0 * @param number number or null * @param msg message or null * @return String AD_PInstance_Log_UU */ public String saveProgress (int id, Timestamp date, BigDecimal number, String msg) { if (log.isLoggable(Level.INFO)) log.info(id + " - " + date + " - " + number + " - " + msg); if (m_pi != null) return m_pi.saveProgress(id, date, number, msg); return ""; } // saveProgress /** * Save Status Log Entry to DB immediately * @param date date or null * @param id record id or 0 * @param number number or null * @param msg message or null * @return String AD_PInstance_Log_UU */ public String saveStatus (int id, Timestamp date, BigDecimal number, String msg) { if (log.isLoggable(Level.INFO)) log.info(id + " - " + date + " - " + number + " - " + msg); if (m_pi != null) return m_pi.saveStatus(id, date, number, msg); return ""; } // saveStatus /** * Update Progress Log Entry with the specified AD_PInstance_Log_UU, update if exists * @param pInstanceLogUU AD_PInstance_Log_UU * @param id record id or 0 * @param date date or null * @param number number or null * @param msg message or null */ public void updateProgress (String pInstanceLogUU, int id, Timestamp date, BigDecimal number, String msg) { if (m_pi != null) m_pi.updateProgress(pInstanceLogUU, id, date, number, msg); if (log.isLoggable(Level.INFO)) log.info(pInstanceLogUU + " - " + id + " - " + date + " - " + number + " - " + msg); } // saveLog /** * Call class method using Java reflection * @param className class * @param methodName method * @param args arguments * @return result */ public Object doIt (String className, String methodName, Object args[]) { try { Class clazz = Class.forName(className); Object object = clazz.getDeclaredConstructor().newInstance(); Method[] methods = clazz.getMethods(); for (int i = 0; i < methods.length; i++) { if (methods[i].getName().equals(methodName)) return methods[i].invoke(object, args); } } catch (Exception ex) { log.log(Level.SEVERE, "doIt", ex); throw new RuntimeException(ex); } return null; } // doIt /** * Lock Process Instance */ private void lock() { if (log.isLoggable(Level.FINE)) log.fine("AD_PInstance_ID=" + m_pi.getAD_PInstance_ID()); try { if(m_pi.getAD_PInstance_ID() > 0) // Update only when AD_PInstance_ID > 0 (When we Start Process w/o saving process instance (No Process Audit)) DB.executeUpdate("UPDATE AD_PInstance SET IsProcessing='Y' WHERE AD_PInstance_ID=" + m_pi.getAD_PInstance_ID(), null); // outside trx } catch (Exception e) { log.severe("lock() - " + e.getLocalizedMessage()); } } // lock /** * Unlock Process Instance.
    * Update Process Instance (AD_PInstance) and write message (ErrorMsg) and state (result). */ private void unlock () { boolean noContext = Env.getCtx().isEmpty() && Env.getCtx().getProperty(Env.AD_CLIENT_ID) == null; try { //save logging info even if context is lost if (noContext) Env.getCtx().put(Env.AD_CLIENT_ID, m_pi.getAD_Client_ID()); //clear interrupt signal so that we can unlock the ad_pinstance record if (Thread.currentThread().isInterrupted()) Thread.interrupted(); if(m_pi.getAD_PInstance_ID() > 0) { MPInstance mpi = new MPInstance (getCtx(), m_pi.getAD_PInstance_ID(), null); if (mpi.get_ID() == 0) { log.log(Level.INFO, "Did not find PInstance " + m_pi.getAD_PInstance_ID()); return; } mpi.setIsProcessing(false); mpi.setResult(!m_pi.isError()); mpi.setErrorMsg(m_pi.getSummary()); mpi.setJsonData(m_pi.getJsonData()); mpi.saveEx(); if (log.isLoggable(Level.FINE)) log.fine(mpi.toString()); ProcessInfoUtil.saveLogToDB(m_pi); } } catch (Exception e) { log.severe("unlock() - " + e.getLocalizedMessage()); } finally { if (noContext) Env.getCtx().remove(Env.AD_CLIENT_ID); } } // unlock /** * Get the main transaction of the current process. * @return the transaction name */ public String get_TrxName() { if (m_trx != null) return m_trx.getTrxName(); return null; } // get_TrxName @Override public void setProcessUI(IProcessUI monitor) { processUI = monitor; } /** * Publish status update message to client * @param message */ protected void statusUpdate(String message) { if (processUI != null) { processUI.statusUpdate(message); } } /** * Attempts to initialize class fields having the {@link Parameter} annotation * with the values received by this process instance. */ private void autoFillParameters(){ Map map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); // detects annotated fields in this class and its super classes Class target = getClass(); while(target != null && !target.equals(SvrProcess.class)) { for (Field field: getFieldsWithParameters(target)) { field.setAccessible(true); Parameter pa = field.getAnnotation(Parameter.class); if(map.containsValue(field)) continue; String name = pa.name().isEmpty() ? field.getName() : pa.name(); map.put(name.toLowerCase(), field); } target = target.getSuperclass(); } if(map.size()==0) return; for(ProcessInfoParameter parameter : getParameter()){ String name = parameter.getParameterName().trim().toLowerCase(); Field field = map.get(name); Field toField = map.containsKey(name + "_to") ? map.get(name + "_to") : null; Field notField = map.containsKey(name + "_not") ? map.get(name + "_not") : null; // try to match fields using the "p_" prefix convention if(field==null) { String candidate = "p_" + name; field = map.get(candidate); toField = map.containsKey(candidate + "_to") ? map.get(candidate + "_to") : null; notField = map.containsKey(candidate + "_not") ? map.get(candidate + "_not") : null; } // try to match fields with same name as metadata declaration after stripping "_" if(field==null) { String candidate = name.replace("_", ""); field = map.get(candidate); toField = map.containsKey(candidate + "to") ? map.get(candidate + "to") : null; notField = map.containsKey(candidate + "not") ? map.get(candidate + "not") : null; } if(field==null) continue; Type type = field.getType(); try{ if (type.equals(Integer.TYPE) || type.equals(Integer.class)) { field.set(this, parameter.getParameterAsInt()); if(parameter.getParameter_To()!=null && toField != null) toField.set(this, parameter.getParameter_ToAsInt()); } else if (type.equals(String.class)) { field.set(this, (String) parameter.getParameter()); if(notField != null) notField.set(this, (boolean) parameter.isNotClause()); } else if (type.equals(java.sql.Timestamp.class)) { field.set(this, (Timestamp) parameter.getParameter()); if(parameter.getParameter_To()!=null && toField != null) toField.set(this, (Timestamp) parameter.getParameter_To()); } else if (type.equals(BigDecimal.class)) { field.set(this, (BigDecimal) parameter.getParameter()); } else if (type.equals(boolean.class) || type.equals(Boolean.class)) { Object tmp = parameter.getParameter(); if(tmp instanceof String && tmp != null) field.set(this, "Y".equals(tmp)); else field.set(this, tmp); } else { continue; } }catch(Exception e){ throw new RuntimeException(e); } } } /** * Tries to find all class fields having the {@link Parameter} annotation. * @param clazz * @return a list of annotated fields */ private List getFieldsWithParameters(Class clazz) { if (clazz != null) return Arrays.stream(clazz.getDeclaredFields()) .filter(f -> f.getAnnotation(Parameter.class) != null) .collect(Collectors.toList()); return Collections.emptyList(); } } // SvrProcess