All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.praxislive.code.CodeContext Maven / Gradle / Ivy

Go to download

Forest-of-actors runtime supporting real-time systems and real-time recoding - bringing aspects of Erlang, Smalltalk and Extempore to Java.

There is a newer version: 6.0.0-beta1
Show newest version
/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2022 Neil C Smith.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version 3 only, as
 * published by the Free Software Foundation.
 *
 * This code 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
 * version 3 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License version 3
 * along with this work; if not, see http://www.gnu.org/licenses/
 *
 *
 * Please visit https://www.praxislive.org if you need additional information or
 * have any questions.
 */
package org.praxislive.code;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import org.praxislive.code.userapi.Async;
import org.praxislive.core.Call;
import org.praxislive.core.Component;
import org.praxislive.core.ComponentAddress;
import org.praxislive.core.Control;
import org.praxislive.core.ControlAddress;
import org.praxislive.core.ExecutionContext;
import org.praxislive.core.Lookup;
import org.praxislive.core.PacketRouter;
import org.praxislive.core.Port;
import org.praxislive.core.ComponentInfo;
import org.praxislive.core.Value;
import org.praxislive.core.services.Service;
import org.praxislive.core.services.Services;
import org.praxislive.core.services.LogBuilder;
import org.praxislive.core.services.LogLevel;
import org.praxislive.core.services.ServiceUnavailableException;
import org.praxislive.core.services.TaskService;
import org.praxislive.core.types.PError;
import org.praxislive.core.types.PReference;

/**
 * A CodeContext wraps each {@link CodeDelegate}, managing state and the
 * transition from one iteration of delegate to the next on behalf of a
 * {@link CodeComponent}.
 *
 * @param  supported delegate type
 */
public abstract class CodeContext {

    private final Map controls;
    private final Map ports;
    private final Map refs;
    private final ComponentInfo info;

    private final D delegate;
    private final LogBuilder log;
    private final Driver driver;
    private final boolean requireClock;
    private final List clockListeners;
    private final ResponseHandler responseHandler;

    private ExecutionContext execCtxt;
    private ExecutionContext.State execState = ExecutionContext.State.NEW;
    private CodeComponent cmp;
    private long time;
    private ControlAddress responseAddress;

    /**
     * Create a CodeContext by processing the provided {@link CodeConnector}
     * (containing CodeDelegate).
     *
     * @param connector code connector with delegate
     */
    protected CodeContext(CodeConnector connector) {
        this(connector, false);
    }

    /**
     * Create a CodeContext by processing the provided {@link CodeConnector}
     * (containing CodeDelegate). This constructor takes a boolean to force
     * connecting the context to the execution clock, should the subtype always
     * require clock signals.
     *
     * @param connector code connector with delegate
     * @param requireClock true to force clock connection
     */
    protected CodeContext(CodeConnector connector, boolean requireClock) {
        this.driver = new Driver();
        clockListeners = new CopyOnWriteArrayList<>();
        // @TODO what is maximum allowed amount a root can be behind system time?
        try {
            connector.process();
            controls = connector.extractControls();
            ports = connector.extractPorts();
            refs = connector.extractRefs();
            info = connector.extractInfo();
            delegate = connector.getDelegate();
            log = new LogBuilder(LogLevel.ERROR);
            this.requireClock = requireClock || connector.requiresClock();
            this.responseHandler = Objects.requireNonNull((ResponseHandler) controls.get(ResponseHandler.ID));
        } catch (Exception e) {
//            Logger.getLogger(CodeContext.class.getName()).log(Level.FINE, "", e);
            throw e;
        }
    }

    void setComponent(CodeComponent cmp) {
        this.cmp = cmp;
        delegate.setContext(this);
    }

    void handleConfigure(CodeComponent cmp, CodeContext oldCtxt) {
        configureControls(oldCtxt);
        configurePorts(oldCtxt);
        configureRefs(oldCtxt);
        configure(cmp, oldCtxt);
    }

    /**
     * A hook method that will be called when the CodeContext is configured on a
     * component. It is called after controls, ports and refs have been
     * configured. Subclasses may override this to do additional configuration.
     * The default implementation does nothing.
     *
     * @param cmp component being attached to
     * @param oldCtxt previous context, or null if there was none
     */
    protected void configure(CodeComponent cmp, CodeContext oldCtxt) {
    }

    private void configureControls(CodeContext oldCtxt) {
        Map oldControls = oldCtxt == null
                ? Collections.emptyMap() : oldCtxt.controls;
        for (Map.Entry entry : controls.entrySet()) {
            ControlDescriptor oldCD = oldControls.remove(entry.getKey());
            if (oldCD != null) {
                entry.getValue().attach(this, oldCD.getControl());
            } else {
                entry.getValue().attach(this, null);
            }
        }
        for (ControlDescriptor oldCD : oldControls.values()) {
            oldCD.dispose();
        }
    }

    private void configurePorts(CodeContext oldCtxt) {
        Map oldPorts = oldCtxt == null
                ? Collections.emptyMap() : oldCtxt.ports;
        for (Map.Entry entry : ports.entrySet()) {
            PortDescriptor oldPD = oldPorts.remove(entry.getKey());
            if (oldPD != null) {
                entry.getValue().attach(this, oldPD.getPort());
            } else {
                entry.getValue().attach(this, null);
            }
        }
        for (PortDescriptor oldPD : oldPorts.values()) {
            oldPD.getPort().disconnectAll();
            oldPD.dispose();
        }
    }

    private void configureRefs(CodeContext oldCtxt) {
        Map oldRefs = oldCtxt == null
                ? Collections.EMPTY_MAP : oldCtxt.refs;
        refs.forEach((id, ref) -> ref.attach(this, oldRefs.remove(id)));
        oldRefs.forEach((id, ref) -> ref.dispose());
    }

    final void handleHierarchyChanged() {
        hierarchyChanged();

        LogLevel level = getLookup().find(LogLevel.class)
                .orElse(LogLevel.ERROR);
        log.setLevel(level);

        ExecutionContext ctxt = cmp == null ? null : cmp.getExecutionContext();
        if (execCtxt != ctxt) {
            if (execCtxt != null) {
                execCtxt.removeStateListener(driver);
                execCtxt.removeClockListener(driver);
            }
            execCtxt = ctxt;
            responseAddress = null;
            if (ctxt != null) {
                responseAddress = ControlAddress.of(cmp.getAddress(), ResponseHandler.ID);
                ctxt.addStateListener(driver);
                if (requireClock) {
                    ctxt.addClockListener(driver);
                }
                handleStateChanged(ctxt, false);
            }
        }
    }

    /**
     * Called when the hierarchy changes, which might be because the component
     * hierarchy has changed (see {@link Component#hierarchyChanged()}), the
     * context has been added or is being removed from the component, or for any
     * other reason that cached information should be invalidated (eg. anything
     * retrieved from the lookup). Subclasses may override this to handle such
     * events / invalidate lookup results. The default implementation does
     * nothing.
     */
    protected void hierarchyChanged() {
    }

    final void handleStateChanged(ExecutionContext source, boolean full) {
        if (execState == source.getState()) {
            return;
        }
        if (source.getState() == ExecutionContext.State.IDLE) {
            stopping(source, full);
        }
        reset(full);
        update(source.getTime());
        execState = source.getState();
        if (execState == ExecutionContext.State.ACTIVE) {
            starting(source, full);
        }
        flush();
    }

    /**
     * Hook called when the execution context is started (moves to state
     * {@link ExecutionContext.State#ACTIVE}) or the context is added to a
     * component within an active execution context. Full start will be true in
     * the former case when the execution context itself is changing state.
     * 

* This method may be overridden in subclasses. The default implementation * delegates to {@link #starting(org.praxislive.core.ExecutionContext)}. * * @param source execution context * @param fullStart whether the context itself is transitioning state */ protected void starting(ExecutionContext source, boolean fullStart) { starting(source); } /** * Hook called when the execution context is started (moves to state * {@link ExecutionContext.State#ACTIVE}) or the context is added to a * component within an active execution context. * * @param source execution context */ protected void starting(ExecutionContext source) { } /** * Hook called when the execution context is stopped (moves away from state * {@link ExecutionContext.State#ACTIVE}) or the context is removed from a * component within an active execution context. Full stop will be true in * the former case when the execution context itself is changing state. *

* This method may be overridden in subclasses. The default implementation * delegates to {@link #stopping(org.praxislive.core.ExecutionContext)}. * * @param source execution context * @param fullStop whether the context itself is transitioning state */ protected void stopping(ExecutionContext source, boolean fullStop) { stopping(source); } /** * Hook called when the execution context is stopped (moves away from state * {@link ExecutionContext.State#ACTIVE}) or the context is removed from a * component within an active execution context. * * @param source execution context */ protected void stopping(ExecutionContext source) { } final void handleTick(ExecutionContext source) { update(source.getTime()); tick(source); flush(); } /** * Hook called by the clock listener on the execution context. The default * implementation does nothing. * * @param source execution context */ protected void tick(ExecutionContext source) { } /** * Reset all control, port and reference descriptors. A full reset generally * happens on execution context state changes as opposed to code change * transitions. Descriptors may handle this differently - eg. clear injected * values or dispose references on full. * * @param full whether reset is full (eg. execution state change) */ protected final void reset(boolean full) { controls.values().forEach(cd -> cd.reset(full)); ports.values().forEach(pd -> pd.reset(full)); refs.values().forEach(rd -> rd.reset(full)); } final void handleDispose() { cmp = null; handleHierarchyChanged(); refs.values().forEach(ReferenceDescriptor::dispose); refs.clear(); controls.values().forEach(ControlDescriptor::dispose); controls.clear(); ports.values().forEach(PortDescriptor::dispose); ports.clear(); dispose(); } /** * Hook called during disposal of code context. The default implementation * does nothing. */ protected void dispose() { } /** * Get the code component this code context is attached to, if there is one. * * @return code component, or null */ public CodeComponent getComponent() { return cmp; } /** * Get the delegate this context wraps. * * @return delegate */ public D getDelegate() { return delegate; } /** * Get the control to handle the specified ID, or null if there isn't one. * * @param id control ID * @return control or null */ protected Control getControl(String id) { ControlDescriptor cd = controls.get(id); return cd == null ? null : cd.getControl(); } /** * Get the control descriptor for the specified ID, or null if there isn't * one. * * @param id control ID * @return control descriptor or null */ protected ControlDescriptor getControlDescriptor(String id) { return controls.get(id); } /** * Get all the available control IDs. * * @return control IDs */ @Deprecated protected String[] getControlIDs() { Set keySet = controls.keySet(); return keySet.toArray(new String[keySet.size()]); } /** * Get the port with the specified ID, or null if there isn't one. * * @param id port ID * @return port or null */ protected Port getPort(String id) { PortDescriptor pd = ports.get(id); return pd == null ? null : pd.getPort(); } /** * Get the port descriptor for the specified ID, or null if there isn't one. * * @param id port ID * @return port descriptor or null */ protected PortDescriptor getPortDescriptor(String id) { return ports.get(id); } /** * Get the available port IDs. * * @return port IDs */ @Deprecated protected String[] getPortIDs() { Set keySet = ports.keySet(); return keySet.toArray(new String[keySet.size()]); } /** * Get component info. * * @return component info */ protected ComponentInfo getInfo() { return info; } /** * Find the address of the passed in control, or null if it does not have * one. * * @param control control to find address for * @return control address or null */ protected ControlAddress getAddress(Control control) { ComponentAddress ad = cmp == null ? null : cmp.getAddress(); if (ad != null) { for (Map.Entry ce : controls.entrySet()) { if (ce.getValue().getControl() == control) { return ControlAddress.of(ad, ce.getKey()); } } } return null; } /** * Get lookup. * * @return lookup */ public Lookup getLookup() { return cmp == null ? Lookup.EMPTY : cmp.getLookup(); } /** * Locate the provided service type, if available. * * @param type service to lookup * @return optional service address */ public Optional locateService(Class type) { return getLookup().find(Services.class).flatMap(s -> s.locate(type)); } /** * Get current time in nanoseconds. * * @return time in nanoseconds */ public long getTime() { return time; } /** * Add a clock listener. Resources used inside code delegates should add a * clock listener rather than listen directly on the execution context. * * @param listener clock listener */ public void addClockListener(ClockListener listener) { clockListeners.add(Objects.requireNonNull(listener)); } /** * Remove a clock listener. * * @param listener to remove */ public void removeClockListener(ClockListener listener) { clockListeners.remove(listener); } /** * Get the execution context, or null if not attached. * * @return execution context, or null */ protected ExecutionContext getExecutionContext() { return cmp == null ? null : cmp.getExecutionContext(); } /** * Check whether the CodeContext is running inside an ExecutionContext with * active state. If the execution context is active, but a transition to * active has not yet been handled in this code context, the state * transition will be triggered. * * @return true if active */ protected boolean checkActive() { if (execState == ExecutionContext.State.ACTIVE) { return true; } if (execCtxt != null) { if (execCtxt.getState() == ExecutionContext.State.ACTIVE) { var parent = getComponent().getParent(); if (parent instanceof CodeComponent) { ((CodeComponent) parent).getCodeContext().checkActive(); } handleStateChanged(execCtxt, true); return execState == ExecutionContext.State.ACTIVE; } } return false; } /** * Update the time in this context to the specified time. A value the same * or behind the current value will be ignored. This method will call all * clock listeners. * * @param time updated time */ protected void update(long time) { if (time - this.time > 0) { this.time = time; clockListeners.forEach(ClockListener::tick); } } /** * Invoke the provided task, if the context is active, and after updating * the clock to the specified time (if later). Any exception will be caught * and logged, and the context will be flushed. * * @param time new clock time * @param task runnable task to execute */ public void invoke(long time, Runnable task) { if (checkActive()) { update(time); try { task.run(); } catch (Exception ex) { log.log(LogLevel.ERROR, ex); } flush(); } } /** * Invoke the provided task and return the result, if the context is active, * and after updating the clock to the specified time (if later). Any * exception will be logged and rethrown, and the context flushed. Throws an * {@link IllegalStateException} if {@link #checkActive()} returns * false. * * @param the result type of method call * @param time new clock time * @param task runnable task to execute * @return result * @throws Exception if unable to compute a result */ public V invokeCallable(long time, Callable task) throws Exception { if (checkActive()) { update(time); try { return task.call(); } catch (Exception ex) { log.log(LogLevel.ERROR, ex); throw ex; } finally { flush(); } } else { throw new IllegalStateException("Component not active"); } } void invoke(long time, Method method, Object... params) { if (checkActive()) { update(time); try { method.invoke(getDelegate(), params); } catch (Exception ex) { if (ex instanceof InvocationTargetException) { Throwable t = ex.getCause(); ex = t instanceof Exception ? (Exception) t : ex; } StringBuilder sb = new StringBuilder("Exception thrown from "); sb.append(method.getName()); sb.append('('); Class[] types = method.getParameterTypes(); for (int i = 0; i < types.length; i++) { sb.append(types[i].getSimpleName()); if (i < (types.length - 1)) { sb.append(','); } } sb.append(')'); log.log(LogLevel.ERROR, ex, sb.toString()); } flush(); } } /** * Flush the code context. By default this message checks for pending log * messages and delivers to the log. */ protected void flush() { if (!log.isEmpty()) { log(log.toList()); log.clear(); } } /** * Get the log builder for writing log messages. * * @return log builder */ public LogBuilder getLog() { return log; } /** * Get the active log level. * * @return active log level */ protected LogLevel getLogLevel() { return log.getLevel(); } /** * Process and send messages from an external log builder. * * @param log external log builder */ protected void log(LogBuilder log) { if (log.isEmpty()) { return; } log(log.toList()); } private void log(List args) { PacketRouter router = cmp.getPacketRouter(); ControlAddress to = cmp.getLogToAddress(); ControlAddress from = responseAddress; if (router == null || to == null) { return; } router.route(Call.createQuiet(to, from, time, args)); } final void tell(ControlAddress destination, Value value) { Call call = Call.createQuiet(destination, responseAddress, getTime(), value); getComponent().getPacketRouter().route(call); } final void tellIn(double seconds, ControlAddress destination, Value value) { long timeCode = getTime() + ((long) (seconds * 1_000_000_000)); Call call = Call.createQuiet(destination, responseAddress, timeCode, value); getComponent().getPacketRouter().route(call); } final Async ask(ControlAddress destination, List args) { Call call = Call.create(destination, responseAddress, time, args); getComponent().getPacketRouter().route(call); Async async = new Async<>(); responseHandler.register(call, async); return async; } final Async async(T input, Async.Task task) { Async async = new Async<>(); try { ControlAddress to = locateService(TaskService.class) .map(c -> ControlAddress.of(c, TaskService.SUBMIT)) .orElseThrow(ServiceUnavailableException::new); TaskService.Task wrapper = () -> PReference.of(task.execute(input)); Call call = Call.create(to, responseAddress, time, PReference.of(wrapper)); getComponent().getPacketRouter().route(call); responseHandler.register(call, async, c -> { @SuppressWarnings("unchecked") R result = (R) PReference.from(c.args().get(0)) .flatMap(ref -> ref.as(Object.class)) .orElseThrow(IllegalStateException::new); return result; }); } catch (Exception ex) { async.fail(PError.of(ex)); } return async; } /** * Listener for responding to time changes inside the context. */ public static interface ClockListener { /** * Time has changed. */ public void tick(); } private class Driver implements ExecutionContext.StateListener, ExecutionContext.ClockListener { @Override public void stateChanged(ExecutionContext source) { handleStateChanged(source, true); } @Override public void tick(ExecutionContext source) { handleTick(source); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy