001/*
002 * jPOS Project [http://jpos.org]
003 * Copyright (C) 2000-2026 jPOS Software SRL
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jpos.transaction.participant;
020
021import org.jdom2.Element;
022import org.jpos.core.ConfigurationException;
023import org.jpos.core.XmlConfigurable;
024import org.jpos.q2.QFactory;
025import org.jpos.transaction.AbortParticipant;
026import org.jpos.transaction.Context;
027import org.jpos.transaction.TransactionParticipant;
028import org.jpos.util.Log;
029
030import javax.script.Invocable;
031import javax.script.ScriptEngine;
032import javax.script.ScriptEngineManager;
033import javax.script.ScriptException;
034import java.io.FileReader;
035import java.io.Serializable;
036
037/**
038 * A TransactionParticipant whose prepare, commit and abort methods can be
039 * specified through JS scripts. <BR>
040 * To indicate what code to execute for any of the methods just add an element
041 *  named 'prepare', 'commit' or 'abort' contained in that of the participant. <BR>
042 *
043 *  The value to return
044 *  in the prepare method should be stored in the script variable named "result".
045 *  None of these tags are mandatory. <BR>
046 *
047 * Usage:
048 *
049 * <pre>
050 *     Add a transaction participant like this:
051 *     &lt;participant class="org.jpos.transaction.participant.JSParticipant" logger="Q2" realm="js"
052 *     src='deploy/test.js' /&gt;
053 *
054 *     test.js may look like this (all functions are optional)
055 *
056 *     var K = Java.type("org.jpos.transaction.TransactionConstants");
057 *     var prepare = function(id, ctx) {
058 *       var map = ctx.getMap();
059 *       ctx.log ("Prepare has been called");
060 *       ctx.log (map.TIMESTAMP);
061 *       map.NEWPROPERTY='ABC';
062 *       return K.PREPARED;
063 *     }
064 *
065 *     var prepareForAbort = function(id, ctx) {
066 *       ctx.put ("Test", "Test from JS transaction $id");
067 *       ctx.log ("prepareForAbort has been called");
068 *       return K.PREPARED;
069 *     }
070 *     var commit = function(id, ctx) {
071 *       ctx.log ("Commit has been called");
072 *     }
073 *
074 *     var abort = function(id, ctx) {
075 *       ctx.log ("Abort has been called");
076 *     }
077 * </pre>
078 *
079 * @author  @apr (based on AMarques' BSHTransactionParticipant)
080 */
081@SuppressWarnings("unchecked")
082public class JSParticipant extends Log
083    implements TransactionParticipant, AbortParticipant, XmlConfigurable 
084{
085    private Invocable js;
086    boolean trace;
087    boolean hasPrepare;
088    boolean hasPrepareForAbort;
089    boolean hasCommit;
090    boolean hasAbort;
091
092    public int prepare (long id, Serializable context) {
093        return hasPrepare ? invokeWithResult("prepare", id, context) : PREPARED | READONLY;
094    }
095    public int prepareForAbort (long id, Serializable context) {
096        return hasPrepareForAbort ? invokeWithResult("prepareForAbort", id, context) : PREPARED | READONLY;
097    }
098    public void commit(long id, Serializable context) {
099        if (hasCommit)
100            invokeNoResult("commit", id, context);
101    }
102
103    public void abort(long id, Serializable context) {
104        if (hasAbort)
105            invokeNoResult("abort", id, context);
106    }
107
108    public void setConfiguration(Element e) throws ConfigurationException {
109            try (FileReader src = new FileReader(QFactory.getAttributeValue(e, "src")))  {
110            trace = "yes".equals(QFactory.getAttributeValue(e, "trace"));
111            ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
112            engine.eval(src);
113            js = (Invocable) engine;
114            hasPrepare = hasFunction ("prepare");
115            hasPrepareForAbort = hasFunction ("prepareForAbort");
116            hasCommit = hasFunction ("commit");
117            hasAbort = hasFunction ("abort");
118        } catch (Exception ex) {
119            throw new ConfigurationException(ex.getMessage(), ex);
120        }
121    }
122
123    private boolean hasFunction (String functionName) throws ConfigurationException {
124        try {
125            js.invokeFunction(functionName, 0L, new Context());
126            return true;
127        } catch (NoSuchMethodException e) {
128            return false;
129        } catch (ScriptException e) {
130            throw new ConfigurationException (e);
131        }
132    }
133
134    private int invokeWithResult (String functionName, long id, Serializable context) {
135        try {
136            return (Integer) js.invokeFunction(functionName, id, context);
137        } catch (Exception e) {
138            if (context instanceof Context) {
139                Context ctx = (Context) context;
140                ctx.log(e);
141            } else {
142                warn(id, e);
143            }
144            return ABORTED;
145        }
146    }
147    private void invokeNoResult (String functionName, long id, Serializable context) {
148        try {
149            js.invokeFunction(functionName, id, context);
150        } catch (Exception e) {
151            if (context instanceof Context) {
152                Context ctx = (Context) context;
153                ctx.log(e);
154            } else {
155                warn(id, e);
156            }
157        }
158    }
159}