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 * <participant class="org.jpos.transaction.participant.JSParticipant" logger="Q2" realm="js" 052 * src='deploy/test.js' /> 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 /** Default constructor; no instance state to initialise. */ 086 public JSParticipant() {} 087 private Invocable js; 088 boolean trace; 089 boolean hasPrepare; 090 boolean hasPrepareForAbort; 091 boolean hasCommit; 092 boolean hasAbort; 093 094 public int prepare (long id, Serializable context) { 095 return hasPrepare ? invokeWithResult("prepare", id, context) : PREPARED | READONLY; 096 } 097 public int prepareForAbort (long id, Serializable context) { 098 return hasPrepareForAbort ? invokeWithResult("prepareForAbort", id, context) : PREPARED | READONLY; 099 } 100 public void commit(long id, Serializable context) { 101 if (hasCommit) 102 invokeNoResult("commit", id, context); 103 } 104 105 public void abort(long id, Serializable context) { 106 if (hasAbort) 107 invokeNoResult("abort", id, context); 108 } 109 110 public void setConfiguration(Element e) throws ConfigurationException { 111 try (FileReader src = new FileReader(QFactory.getAttributeValue(e, "src"))) { 112 trace = "yes".equals(QFactory.getAttributeValue(e, "trace")); 113 ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); 114 engine.eval(src); 115 js = (Invocable) engine; 116 hasPrepare = hasFunction ("prepare"); 117 hasPrepareForAbort = hasFunction ("prepareForAbort"); 118 hasCommit = hasFunction ("commit"); 119 hasAbort = hasFunction ("abort"); 120 } catch (Exception ex) { 121 throw new ConfigurationException(ex.getMessage(), ex); 122 } 123 } 124 125 private boolean hasFunction (String functionName) throws ConfigurationException { 126 try { 127 js.invokeFunction(functionName, 0L, new Context()); 128 return true; 129 } catch (NoSuchMethodException e) { 130 return false; 131 } catch (ScriptException e) { 132 throw new ConfigurationException (e); 133 } 134 } 135 136 private int invokeWithResult (String functionName, long id, Serializable context) { 137 try { 138 return (Integer) js.invokeFunction(functionName, id, context); 139 } catch (Exception e) { 140 if (context instanceof Context) { 141 Context ctx = (Context) context; 142 ctx.log(e); 143 } else { 144 warn(id, e); 145 } 146 return ABORTED; 147 } 148 } 149 private void invokeNoResult (String functionName, long id, Serializable context) { 150 try { 151 js.invokeFunction(functionName, id, context); 152 } catch (Exception e) { 153 if (context instanceof Context) { 154 Context ctx = (Context) context; 155 ctx.log(e); 156 } else { 157 warn(id, e); 158 } 159 } 160 } 161}