diff --git a/exist-core/src/main/java/org/exist/backup/Restore.java b/exist-core/src/main/java/org/exist/backup/Restore.java index 7b38622b112..9c4de22c58a 100644 --- a/exist-core/src/main/java/org/exist/backup/Restore.java +++ b/exist-core/src/main/java/org/exist/backup/Restore.java @@ -76,9 +76,11 @@ public void restore(final DBBroker broker, @Nullable final Txn transaction, fina } // continue restore - final XMLReaderPool parserPool = broker.getBrokerPool().getParserPool(); + final XMLReaderPool parserPool = broker.getBrokerPool().getXmlReaderPool(); + final boolean triggersEnabled = broker.isTriggersEnabled(); XMLReader reader = null; try { + broker.setTriggersEnabled(false); reader = parserPool.borrowXMLReader(); listener.started(totalNrOfFiles); @@ -99,10 +101,14 @@ public void restore(final DBBroker broker, @Nullable final Txn transaction, fina } } finally { - listener.finished(); + try { + listener.finished(); + } finally { + broker.setTriggersEnabled(triggersEnabled); - if (reader != null) { - parserPool.returnXMLReader(reader); + if (reader != null) { + parserPool.returnXMLReader(reader); + } } } } diff --git a/exist-core/src/main/java/org/exist/collections/triggers/TriggerStatePerThread.java b/exist-core/src/main/java/org/exist/collections/triggers/TriggerStatePerThread.java index b45d50e0696..4c27823becb 100644 --- a/exist-core/src/main/java/org/exist/collections/triggers/TriggerStatePerThread.java +++ b/exist-core/src/main/java/org/exist/collections/triggers/TriggerStatePerThread.java @@ -21,12 +21,23 @@ */ package org.exist.collections.triggers; +import org.exist.storage.txn.Txn; +import org.exist.storage.txn.TxnListener; import org.exist.xmldb.XmldbURI; import javax.annotation.Nullable; +import java.lang.ref.WeakReference; import java.util.ArrayDeque; +import java.util.Collections; import java.util.Deque; +import java.util.HashSet; import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.WeakHashMap; +import java.util.function.BiConsumer; +import java.util.function.Consumer; /** * Avoid infinite recursions in Triggers by preventing the same trigger @@ -35,278 +46,258 @@ * @author Adam Retter */ public class TriggerStatePerThread { - - private final static ThreadLocal> THREAD_LOCAL_STATES = ThreadLocal.withInitial(ArrayDeque::new); - - public static void setAndTest(final Trigger trigger, final TriggerPhase triggerPhase, final TriggerEvent triggerEvent, final XmldbURI src, final @Nullable XmldbURI dst) throws CyclicTriggerException { - final Deque states = THREAD_LOCAL_STATES.get(); - - if (states.isEmpty()) { - if (triggerPhase != TriggerPhase.BEFORE) { - throw new IllegalStateException("The Before phase of a trigger must occur before the After phase"); - } - states.addFirst(new TriggerState(trigger, triggerPhase, triggerEvent, src, dst)); - return; - } - - TriggerState prevState = states.peekFirst(); - - // is the new state the same as the previous state (excluding the phase) - if (prevState.equalsIgnoringPhase(trigger, triggerEvent, src, dst)) { - - // is this the after phase (i.e. matching completion) of a previous non-cyclic before phase? - if (triggerPhase == TriggerPhase.AFTER) { - - int skipBefores = 0; - - for (final Iterator it = states.iterator(); it.hasNext(); ) { - prevState = it.next(); - - // travel up, first "Before" we encounter - we should check if (a) that we complete it, and/or (b) is non-cyclic (if not we are also cyclic) - if (prevState.triggerPhase == TriggerPhase.BEFORE) { - - if (skipBefores > 0) { - skipBefores--; - - } else { - if (prevState.isCompletedBy(trigger, triggerPhase, triggerEvent, src, dst)) { - if (prevState instanceof PossibleCyclicTriggerState) { - // if the Before phase is a PossibleCyclicTriggerState then this completing After phase must also be a PossibleCyclicTriggerState - final TriggerState newState = new PossibleCyclicTriggerState(trigger, triggerPhase, triggerEvent, src, dst); - states.addFirst(newState); - - throw new CyclicTriggerException("Detected Matching possible cyclic trigger event for After phase (" + newState + ") of previous Before phase (" + prevState + ")"); - - } else { - // if the Before Phase is NOT a PossibleCyclicTriggerState, then neither is this completing After phase... - states.addFirst(new TriggerState(trigger, triggerPhase, triggerEvent, src, dst)); - return; - } - - } else { - throw new IllegalStateException("Cannot interleave Trigger states"); - } - } - } else if (prevState.triggerPhase == TriggerPhase.AFTER) { - skipBefores++; - } - } - - throw new IllegalStateException("Could not find a matching Before phase for After phase"); - - } else { - // it's a cyclic exception! - final TriggerState newState = new PossibleCyclicTriggerState(trigger, triggerPhase, triggerEvent, src, dst); - states.addFirst(newState); - - throw new CyclicTriggerException("Detected possible cyclic trigger events: " + newState); - } - } - - states.addFirst(new TriggerState(trigger, triggerPhase, triggerEvent, src, dst)); - } - - public static class CyclicTriggerException extends Exception { - public CyclicTriggerException(final String message) { - super(message); - } - } - - public static void clearIfFinished(final TriggerPhase phase) { - if (phase == TriggerPhase.AFTER) { - - int depth = 0; - final Deque states = THREAD_LOCAL_STATES.get(); - for (final Iterator it = states.descendingIterator(); it.hasNext(); ) { - final TriggerState state = it.next(); - switch (state.triggerPhase) { - case BEFORE: - depth++; - break; - case AFTER: - depth--; - break; - default: - throw new IllegalStateException("Unknown phase: " + state.triggerPhase + "for trigger state: " + state); - } - } - - if (depth == 0) { - clear(); - } - } - } - - public static void clear() { - THREAD_LOCAL_STATES.remove(); - } - - public static boolean isEmpty() { - return THREAD_LOCAL_STATES.get().isEmpty(); - } - - private static class PossibleCyclicTriggerState extends TriggerState { - public PossibleCyclicTriggerState(final TriggerState triggerState) { - super(triggerState.trigger, triggerState.triggerPhase, triggerState.triggerEvent, triggerState.src, triggerState.dst); - } - - public PossibleCyclicTriggerState(final Trigger trigger, final TriggerPhase triggerPhase, final TriggerEvent triggerEvent, final XmldbURI src, final @Nullable XmldbURI dst) { - super(trigger, triggerPhase, triggerEvent, src, dst); - } - } - - private static class TriggerState { - private final Trigger trigger; - private final TriggerPhase triggerPhase; - private final TriggerEvent triggerEvent; - private final XmldbURI src; - private final @Nullable XmldbURI dst; - - public TriggerState(final Trigger trigger, final TriggerPhase triggerPhase, final TriggerEvent triggerEvent, final XmldbURI src, final @Nullable XmldbURI dst) { - this.trigger = trigger; - this.triggerPhase = triggerPhase; - this.triggerEvent = triggerEvent; - this.src = src; - this.dst = dst; - } - - @Override - public String toString() { - final StringBuilder builder = new StringBuilder(); - builder.append(triggerPhase); - builder.append(' '); - builder.append(triggerEvent); - builder.append('('); - if (triggerPhase == TriggerPhase.AFTER && dst != null) { - builder.append(dst); - builder.append(", "); - } - builder.append(src); - if (triggerPhase == TriggerPhase.BEFORE && dst != null) { - builder.append(", "); - builder.append(dst); - } - builder.append(')'); - builder.append(": "); - builder.append(trigger.getClass().getSimpleName()); - if (trigger instanceof XQueryTrigger) { - final String urlQuery = ((XQueryTrigger) trigger).getUrlQuery(); - if (urlQuery != null && !urlQuery.isEmpty()) { - builder.append('('); - builder.append(urlQuery); - builder.append(')'); - } - } - return builder.toString(); - } - - @Override - public boolean equals(final Object o) { - return equals(o, false); - } - - public boolean equalsIgnoringPhase(final Object o) { - return equals(o, true); - } - - private boolean equals(final Object o, final boolean ignorePhase) { - if (this == o) { - return true; - } - - if (o == null || getClass() != o.getClass()) { - return false; - } - - final TriggerState that = (TriggerState) o; - - if (!trigger.equals(that.trigger)) { - return false; - } - - if (!ignorePhase) { - if (triggerPhase != that.triggerPhase) { - return false; - } - } - - if (triggerEvent != that.triggerEvent) { - return false; - } - - if (!src.equals(that.src)) { - return false; - } - - return dst != null ? dst.equals(that.dst) : that.dst == null; - } - - private boolean equalsIgnoringPhase(final Trigger otherTrigger, final TriggerEvent otherTriggerEvent, final XmldbURI otherSrc, @Nullable final XmldbURI otherDst) { - if (!trigger.equals(otherTrigger)) { - return false; - } - - if (triggerEvent != otherTriggerEvent) { - return false; - } - - if (!src.equals(otherSrc)) { - return false; - } - - return dst != null ? dst.equals(otherDst) : otherDst == null; - } - - public boolean isCompletedBy(final Trigger otherTrigger, final TriggerPhase otherTriggerPhase, final TriggerEvent otherTriggerEvent, final XmldbURI otherSrc, @Nullable final XmldbURI otherDst) { - if (this.triggerPhase != TriggerPhase.BEFORE - || otherTriggerPhase != TriggerPhase.AFTER) { - return false; - } - - if (!trigger.equals(otherTrigger)) { - return false; - } - - if (triggerEvent != otherTriggerEvent) { - return false; - } - - if (!src.equals(otherSrc)) { - return false; - } - - return dst != null ? dst.equals(otherDst) : otherDst == null; - } - - public boolean completes(final Object o) { - if (this == o) { - return false; - } - - if (o == null || getClass() != o.getClass()) { - return false; - } - - final TriggerState that = (TriggerState) o; - - if (this.triggerPhase != TriggerPhase.AFTER - || that.triggerPhase != TriggerPhase.BEFORE) { - return false; - } - - if (!trigger.equals(that.trigger)) { - return false; - } - - if (triggerEvent != that.triggerEvent) { - return false; - } - - if (!src.equals(that.src)) { - return false; - } - - return dst != null ? dst.equals(that.dst) : that.dst == null; - } - } + private static final Map TRIGGER_STATES = Collections.synchronizedMap(new WeakHashMap<>()); + + public static void setAndTest(final Txn txn, final Trigger trigger, final TriggerPhase triggerPhase, final TriggerEvent triggerEvent, final XmldbURI src, final @Nullable XmldbURI dst) throws CyclicTriggerException { + final TriggerStates states = getStates(txn); + + if (states.isEmpty()) { + if (triggerPhase != TriggerPhase.BEFORE) { + throw new IllegalStateException("The Before phase of a trigger must occur before the After phase"); + } + states.addFirst(new TriggerState(trigger, triggerPhase, triggerEvent, src, dst, false)); + return; + } + + TriggerState prevState = states.peekFirst(); + + // is the new state the same as the previous state (excluding the phase) + if (prevState.equalsIgnoringPhase(trigger, triggerEvent, src, dst)) { + + // is this the after phase (i.e. matching completion) of a previous non-cyclic before phase? + if (triggerPhase == TriggerPhase.AFTER) { + + int skipBefores = 0; + + for (final Iterator it = states.iterator(); it.hasNext(); ) { + prevState = it.next(); + + // travel up, first "Before" we encounter - we should check if (a) that we complete it, and/or (b) is non-cyclic (if not we are also cyclic) + if (prevState.triggerPhase == TriggerPhase.BEFORE) { + + if (skipBefores > 0) { + skipBefores--; + + } else { + if (prevState.isCompletedBy(trigger, triggerPhase, triggerEvent, src, dst)) { + if (prevState.possiblyCyclic()) { + // if the Before phase is a PossibleCyclicTriggerState then this completing After phase must also be a PossibleCyclicTriggerState + final TriggerState newState = new TriggerState(trigger, triggerPhase, triggerEvent, src, dst, true); + states.addFirst(newState); + + throw new CyclicTriggerException("Detected Matching possible cyclic trigger event for After phase (" + newState + ") of previous Before phase (" + prevState + ")"); + + } else { + // if the Before Phase is NOT a PossibleCyclicTriggerState, then neither is this completing After phase... + states.addFirst(new TriggerState(trigger, triggerPhase, triggerEvent, src, dst, false)); + return; + } + + } else { + throw new IllegalStateException("Cannot interleave Trigger states"); + } + } + } else if (prevState.triggerPhase == TriggerPhase.AFTER) { + skipBefores++; + } + } + + throw new IllegalStateException("Could not find a matching Before phase for After phase"); + + } else { + // it's a cyclic exception! + final TriggerState newState = new TriggerState(trigger, triggerPhase, triggerEvent, src, dst, true); + states.addFirst(newState); + + throw new CyclicTriggerException("Detected possible cyclic trigger events: " + newState); + } + } + + states.addFirst(new TriggerState(trigger, triggerPhase, triggerEvent, src, dst, false)); + } + + public static class CyclicTriggerException extends Exception { + public CyclicTriggerException(final String message) { + super(message); + } + } + + public static void clearIfFinished(final Txn txn, final TriggerPhase phase) { + if (phase == TriggerPhase.AFTER) { + + int depth = 0; + final TriggerStates states = getStates(txn); + for (final Iterator it = states.descendingIterator(); it.hasNext(); ) { + final TriggerState state = it.next(); + switch (state.triggerPhase) { + case BEFORE: + depth++; + break; + case AFTER: + depth--; + break; + default: + throw new IllegalStateException("Unknown phase: " + state.triggerPhase + "for trigger state: " + state); + } + } + + if (depth == 0) { + clear(txn); + } + } + } + + public static int keys() { + return TRIGGER_STATES.size(); + } + + public static void clearAll() { + TRIGGER_STATES.clear(); + } + + public static void clear(final Txn txn) { + TRIGGER_STATES.remove(Thread.currentThread()); + } + + public static boolean isEmpty(final Txn txn) { + return getStates(txn).isEmpty(); + } + + public static void dumpTriggerStates() { + TRIGGER_STATES.forEach((k, s) -> System.err.format("key: %s, size: %s", k, s.size()).println()); + } + + public static void forEach(BiConsumer action) { + TRIGGER_STATES.forEach(action); + } + + private static TriggerStates getStates(final Txn txn) { + return TRIGGER_STATES.computeIfAbsent(Thread.currentThread(), key -> new TriggerStates()); + } + + private static TriggerStates initStates(final Txn txn) { + txn.registerListener(new TransactionCleanUp(txn, TriggerStatePerThread::clear)); + return new TriggerStates(); + } + + public record TransactionCleanUp(Txn txn, Consumer consumer) implements TxnListener { + @Override + public void commit() { + consumer.accept(txn); + } + + @Override + public void abort() { + consumer.accept(txn); + } + } + + public static final class TriggerStates extends ArrayDeque { + } + + public static final class TriggerStatesX extends WeakReference> { + public TriggerStatesX() { + super(new ArrayDeque<>()); + } + + Optional> states() { + return Optional.ofNullable(get()); + } + + public Iterator descendingIterator() { + return states().map(Deque::descendingIterator).orElseGet(Collections::emptyIterator); + } + + public boolean isEmpty() { + return states().map(Deque::isEmpty).orElse(true); + } + + public int size() { + return states().map(Deque::size).orElse(0); + } + + public Iterator iterator() { + return states().map(Deque::iterator).orElseGet(Collections::emptyIterator); + } + + public TriggerState peekFirst() { + return states().map(Deque::peekFirst).orElse(null); + } + + public void addFirst(TriggerState newState) { + states().ifPresent(states -> states.addFirst(newState)); + } + } + + public record TriggerState(Trigger trigger, TriggerPhase triggerPhase, TriggerEvent triggerEvent, XmldbURI src, + @Nullable XmldbURI dst, boolean possiblyCyclic) { + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(triggerPhase); + builder.append(' '); + builder.append(triggerEvent); + builder.append('('); + if (triggerPhase == TriggerPhase.AFTER && dst != null) { + builder.append(dst); + builder.append(", "); + } + builder.append(src); + if (triggerPhase == TriggerPhase.BEFORE && dst != null) { + builder.append(", "); + builder.append(dst); + } + builder.append(')'); + builder.append(": "); + builder.append(trigger.getClass().getSimpleName()); + if (trigger instanceof XQueryTrigger queryTrigger) { + final String urlQuery = queryTrigger.getUrlQuery(); + if (urlQuery != null && !urlQuery.isEmpty()) { + builder.append('('); + builder.append(urlQuery); + builder.append(')'); + } + } + return builder.toString(); + } + + + boolean equalsIgnoringPhase(final Trigger otherTrigger, final TriggerEvent otherTriggerEvent, final XmldbURI otherSrc, @Nullable final XmldbURI otherDst) { + if (!trigger.equals(otherTrigger)) { + return false; + } + + if (triggerEvent != otherTriggerEvent) { + return false; + } + + if (!src.equals(otherSrc)) { + return false; + } + + return Objects.equals(dst, otherDst); + } + + public boolean isCompletedBy(final Trigger otherTrigger, final TriggerPhase otherTriggerPhase, final TriggerEvent otherTriggerEvent, final XmldbURI otherSrc, @Nullable final XmldbURI otherDst) { + if (this.triggerPhase != TriggerPhase.BEFORE + || otherTriggerPhase != TriggerPhase.AFTER) { + return false; + } + + if (!trigger.equals(otherTrigger)) { + return false; + } + + if (triggerEvent != otherTriggerEvent) { + return false; + } + + if (!src.equals(otherSrc)) { + return false; + } + + return Objects.equals(dst, otherDst); + } + } } diff --git a/exist-core/src/main/java/org/exist/collections/triggers/XQueryTrigger.java b/exist-core/src/main/java/org/exist/collections/triggers/XQueryTrigger.java index ddde971e207..c53d02323ba 100644 --- a/exist-core/src/main/java/org/exist/collections/triggers/XQueryTrigger.java +++ b/exist-core/src/main/java/org/exist/collections/triggers/XQueryTrigger.java @@ -119,7 +119,11 @@ public class XQueryTrigger extends SAXTrigger implements DocumentTrigger, Collec private String bindingPrefix = null; private XQuery service; - public final static String PREPARE_EXCEPTION_MESSAGE = "Error during trigger prepare"; + public static final String PREPARE_EXCEPTION_MESSAGE = "Error during trigger prepare"; + + public XQueryTrigger() { + XQueryTriggerMBeanImpl.init(); + } @Override public void configure(final DBBroker broker, final Txn transaction, final Collection parent, final Map> parameters) throws TriggerException { @@ -227,7 +231,7 @@ private void prepare(final TriggerEvent event, final DBBroker broker, final Txn // avoid infinite recursion try { - TriggerStatePerThread.setAndTest(this, TriggerPhase.BEFORE, event, src, dst); + TriggerStatePerThread.setAndTest(transaction,this, TriggerPhase.BEFORE, event, src, dst); } catch (final TriggerStatePerThread.CyclicTriggerException e) { LOG.warn(e.getMessage()); return; @@ -241,7 +245,7 @@ private void prepare(final TriggerEvent event, final DBBroker broker, final Txn declareExternalVariables(context, TriggerPhase.BEFORE, event, src, dst, isCollection); } catch (final XPathException | IOException | PermissionDeniedException e) { - TriggerStatePerThread.clear(); + TriggerStatePerThread.clear(transaction); throw new TriggerException(PREPARE_EXCEPTION_MESSAGE, e); } @@ -255,7 +259,7 @@ private void prepare(final TriggerEvent event, final DBBroker broker, final Txn LOG.debug("Trigger fired for prepare"); } } catch (final XPathException | PermissionDeniedException e) { - TriggerStatePerThread.clear(); + TriggerStatePerThread.clear(transaction); throw new TriggerException(PREPARE_EXCEPTION_MESSAGE, e); } finally { context.runCleanupTasks(); @@ -271,7 +275,7 @@ private void finish(final TriggerEvent event, final DBBroker broker, final Txn t // avoid infinite recursion try { - TriggerStatePerThread.setAndTest(this, TriggerPhase.AFTER, event, src, dst); + TriggerStatePerThread.setAndTest(transaction,this, TriggerPhase.AFTER, event, src, dst); } catch (final TriggerStatePerThread.CyclicTriggerException e) { LOG.warn(e.getMessage()); return; @@ -305,7 +309,7 @@ private void finish(final TriggerEvent event, final DBBroker broker, final Txn t context.runCleanupTasks(); } - TriggerStatePerThread.clearIfFinished(TriggerPhase.AFTER); + TriggerStatePerThread.clearIfFinished(transaction, TriggerPhase.AFTER); if (LOG.isDebugEnabled()) { LOG.debug("Trigger fired for finish"); @@ -393,10 +397,11 @@ private CompiledXQuery getScript(final DBBroker broker, final Txn transaction) t } private void execute(final TriggerPhase phase, final TriggerEvent event, final DBBroker broker, final Txn transaction, final QName functionName, final XmldbURI src, final XmldbURI dst) throws TriggerException { + System.err.format("phase: %s, event: %s, tx: %s, thread: %s", phase, event, transaction, Thread.currentThread()).println(); // avoid infinite recursion try { - TriggerStatePerThread.setAndTest(this, phase, event, src, dst); + TriggerStatePerThread.setAndTest(transaction, this, phase, event, src, dst); } catch (final TriggerStatePerThread.CyclicTriggerException e) { LOG.warn("Skipping Trigger: {}", e.getMessage()); return; @@ -414,7 +419,7 @@ private void execute(final TriggerPhase phase, final TriggerEvent event, final D return; } } catch (final TriggerException e) { - TriggerStatePerThread.clear(); + TriggerStatePerThread.clear(transaction); throw e; } @@ -454,14 +459,14 @@ private void execute(final TriggerPhase phase, final TriggerEvent event, final D } } - TriggerStatePerThread.clear(); + TriggerStatePerThread.clear(transaction); throw new TriggerException(PREPARE_EXCEPTION_MESSAGE, e); } finally { compiledQuery.reset(); context.runCleanupTasks(); } - TriggerStatePerThread.clearIfFinished(phase); + TriggerStatePerThread.clearIfFinished(transaction, phase); if (LOG.isDebugEnabled()) { if (phase == TriggerPhase.AFTER) { diff --git a/exist-core/src/main/java/org/exist/collections/triggers/XQueryTriggerMBean.java b/exist-core/src/main/java/org/exist/collections/triggers/XQueryTriggerMBean.java new file mode 100644 index 00000000000..2439eb40da9 --- /dev/null +++ b/exist-core/src/main/java/org/exist/collections/triggers/XQueryTriggerMBean.java @@ -0,0 +1,32 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.collections.triggers; + +public interface XQueryTriggerMBean { + int getKeys(); + + void clear(); + + String dumpTriggerStates(); + + String listKeys(); +} diff --git a/exist-core/src/main/java/org/exist/collections/triggers/XQueryTriggerMBeanImpl.java b/exist-core/src/main/java/org/exist/collections/triggers/XQueryTriggerMBeanImpl.java new file mode 100644 index 00000000000..0f8310f1faa --- /dev/null +++ b/exist-core/src/main/java/org/exist/collections/triggers/XQueryTriggerMBeanImpl.java @@ -0,0 +1,75 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.collections.triggers; + +import javax.management.InstanceAlreadyExistsException; +import javax.management.MBeanRegistrationException; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.NotCompliantMBeanException; +import javax.management.ObjectName; +import javax.management.StandardMBean; +import java.lang.management.ManagementFactory; +import java.util.StringJoiner; + +final class XQueryTriggerMBeanImpl extends StandardMBean implements XQueryTriggerMBean { + + private XQueryTriggerMBeanImpl() throws NotCompliantMBeanException { + super(XQueryTriggerMBean.class); + } + + static void init() { + try { + final ObjectName name = ObjectName.getInstance("org.exist.management.exist", "type", "TriggerStates"); + final MBeanServer platformMBeanServer = ManagementFactory.getPlatformMBeanServer(); + if (!platformMBeanServer.isRegistered(name)) { + platformMBeanServer.registerMBean(new XQueryTriggerMBeanImpl(), name); + } + } catch (final MalformedObjectNameException | InstanceAlreadyExistsException | MBeanRegistrationException | NotCompliantMBeanException ex) { + ex.printStackTrace(); + } + } + + @Override + public int getKeys() { + return TriggerStatePerThread.keys(); + } + + @Override + public void clear() { + TriggerStatePerThread.clearAll(); + } + + @Override + public String dumpTriggerStates() { + StringJoiner joiner = new StringJoiner("\n"); + TriggerStatePerThread.forEach((k, v) -> joiner.add("%s: %s".formatted(k, v.size()))); + return joiner.toString(); + } + + @Override + public String listKeys() { + StringJoiner joiner = new StringJoiner("\n"); + TriggerStatePerThread.forEach((k, v) -> joiner.add("%s".formatted(k))); + return joiner.toString(); + } +}