Package org.jenkinsci.plugins.workflow.cps

Source Code of org.jenkinsci.plugins.workflow.cps.CpsFlowExecution$ConverterImpl

/*
* The MIT License
*
* Copyright (c) 2013-2014, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package org.jenkinsci.plugins.workflow.cps;

import com.cloudbees.groovy.cps.Continuable;
import com.cloudbees.groovy.cps.Env;
import com.cloudbees.groovy.cps.Envs;
import com.cloudbees.groovy.cps.Outcome;
import com.cloudbees.groovy.cps.impl.ConstantBlock;
import com.cloudbees.groovy.cps.impl.ThrowBlock;
import com.cloudbees.groovy.cps.sandbox.DefaultInvoker;
import com.cloudbees.groovy.cps.sandbox.SandboxInvoker;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.XStreamException;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.converters.reflection.ReflectionProvider;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.mapper.Mapper;
import groovy.lang.GroovyShell;
import hudson.model.Action;
import hudson.model.Result;
import hudson.util.Iterators;
import jenkins.model.CauseOfInterruption;
import jenkins.model.Jenkins;
import org.jboss.marshalling.Unmarshaller;
import org.jenkinsci.plugins.workflow.actions.ErrorAction;
import org.jenkinsci.plugins.workflow.cps.persistence.PersistIn;
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
import org.jenkinsci.plugins.workflow.flow.GraphListener;
import org.jenkinsci.plugins.workflow.graph.BlockEndNode;
import org.jenkinsci.plugins.workflow.graph.BlockStartNode;
import org.jenkinsci.plugins.workflow.graph.FlowEndNode;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.graph.FlowStartNode;
import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException;
import org.jenkinsci.plugins.workflow.steps.StepContext;
import org.jenkinsci.plugins.workflow.steps.StepExecution;
import org.jenkinsci.plugins.workflow.support.concurrent.Futures;
import org.jenkinsci.plugins.workflow.support.pickles.serialization.PickleResolver;
import org.jenkinsci.plugins.workflow.support.pickles.serialization.RiverReader;
import org.jenkinsci.plugins.workflow.support.storage.FlowNodeStorage;
import org.jenkinsci.plugins.workflow.support.storage.SimpleXStreamFlowNodeStorage;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.Stack;
import java.util.TreeMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.Phaser;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

import static com.thoughtworks.xstream.io.ExtendedHierarchicalStreamWriterHelper.*;
import hudson.model.User;
import hudson.security.ACL;
import javax.annotation.CheckForNull;
import javax.annotation.concurrent.GuardedBy;

import org.acegisecurity.Authentication;
import org.acegisecurity.userdetails.UsernameNotFoundException;
import static org.jenkinsci.plugins.workflow.cps.persistence.PersistenceContext.*;

/**
* {@link FlowExecution} implemented with Groovy CPS.
*
* <h2>State Transition</h2>
* <p>
* {@link CpsFlowExecution} goes through the following states:
*
* <pre>
*                                    +----------------------+
*                                    |                      |
*                                    v                      |
* PERSISTED --> PREPARING --> SUSPENDED --> RUNNABLE --> RUNNING --> COMPLETE
*                                             ^
*                                             |
*                                           INITIAL
* </pre>
*
* <dl>
* <dt>INITIAL</dt>
* <dd>
*     When a new {@link CpsFlowExecution} is created, it starts from here.
*
*     When {@link #start()} method is called, we get one thread scheduled, and we arrive at RUNNABLE state.
* </dd>
* <dt>PERSISTED</dt>
* <dd>
*     {@link CpsFlowExecution} is on disk with its owner, for example in <tt>build.xml</tt> of the workflow run.
*     Nothing exists in memory. For example, Jenkins is not running.
*
*     Transition from this into PREPARING is triggered outside our control by XStream using
*     {@link ConverterImpl} to unmarshal {@link CpsFlowExecution}. {@link #onLoad()} is called
*     at the end, and we arrive at the PREPARING state.
* </dd>
* <dt>PREPARING</dt>
* <dd>
*     {@link CpsFlowExecution} is in memory, but {@link CpsThreadGroup} isn't. We are trying to
*     restore all the ephemeral pickles that are necessary to get workflow going again.
*     {@link #programPromise} represents a promise of completing this state.
*
*     {@link PickleResolver} keeps track of this, and when it's all done, we arrive at SUSPENDED state.
* </dd>
* <dt>SUSPENDED</dt>
* <dd>
*     {@link CpsThreadGroup} is in memory, but all {@link CpsThread}s are {@linkplain CpsThread#isRunnable() not runnable},
*     which means they are waiting for some conditions to trigger (such as a completion of a shell script that's executing,
*     human approval, etc). {@link CpsFlowExecution} and {@link CpsThreadGroup} are safe to persist.
*
*     When a condition is met, {@link CpsThread#resume(Outcome)} is called, and that thread becomes runnable,
*     and we move to the RUNNABLE state.
* </dd>
* <dt>RUNNABLE</dt>
* <dd>
*     Some of {@link CpsThread}s are runnable, but we aren't actually running. The conditions that triggered
*     {@link CpsThread} is captured in {@link CpsThread#resumeValue}.
*     As we get into this state, {@link CpsThreadGroup#scheduleRun()}
*     should be called to schedule the execution.
*     {@link CpsFlowExecution} and {@link CpsThreadGroup} are safe to persist in this state, just like in the SUSPENDED state.
*
*     When {@link CpsThreadGroup#runner} allocated a real Java thread to the execution, we move to the RUNNING state.
* </dd>
* <dt>RUNNING</dt>
* <dd>
*     A thread is inside {@link CpsThreadGroup#run()} and is actively mutating the object graph inside the script.
*     This state continues until no threads are runnable any more.
*     Only one thread executes {@link CpsThreadGroup#run()}.
*
*     In this state, {@link CpsFlowExecution} still need to be persistable (because generally we don't get to
*     control when it is persisted), but {@link CpsThreadGroup} isn't safe to persist.
*
*     When the Java thread leaves {@link CpsThreadGroup#run()}, we move to the SUSPENDED state.
* </dd>
* <dt>COMPLETE</dt>
* <dd>
*     All the {@link CpsThread}s have terminated and there's nothing more to execute, and there's no more events to wait.
*     The result is finalized and there's no further state change.
* </dd>
* </dl>
*
* @author Kohsuke Kawaguchi
*/
@PersistIn(RUN)
public class CpsFlowExecution extends FlowExecution {
    /**
     * Groovy script of the main source file (that the user enters in the GUI)
     */
    private final String script;

    /**
     * Any additional scripts {@linkplain CpsGroovyShell#parse(String) parsed} afterward, keyed by
     * their FQCN.
     */
    /*package*/ /*final*/ Map<String,String> loadedScripts = new HashMap<String, String>();

    private final boolean sandbox;
    private /*almost final*/ FlowExecutionOwner owner;

    /**
     * Loading of the program is asynchronous because it requires us to re-obtain stateful objects.
     * This object represents a {@link Future} for filling in {@link CpsThreadGroup}.
     *
     * TODO: provide a mechanism to diagnose how far along this process is.
     *
     * @see #runInCpsVmThread(FutureCallback)
     */
    public transient volatile ListenableFuture<CpsThreadGroup> programPromise;

    /**
     * Recreated from {@link #owner}
     */
    /*package*/ transient /*almos final*/ FlowNodeStorage storage;

    /** User ID associated with this build, or null if none specific. */
    private final @CheckForNull String user;

    /**
     * Start nodes that have been created, whose {@link BlockEndNode} is not yet created.
     */
    @GuardedBy("this")
    /*package*/ final Stack<BlockStartNode> startNodes = new Stack<BlockStartNode>();

    @GuardedBy("this")
    private final NavigableMap<Integer,FlowHead> heads = new TreeMap();

    private final AtomicInteger iota = new AtomicInteger();

    private transient List<GraphListener> listeners;

    /**
     * Result set from {@link StepContext}. Start by success and progressively gets worse.
     */
    private Result result = Result.SUCCESS;

    /**
     * When the program is completed, set to true.
     *
     * {@link FlowExecution} gets loaded into memory for the build records that have been completed,
     * and for those we don't want to load the program state, so that check should be efficient.
     */
    private boolean done;

    /**
     * Groovy compiler with CPS+sandbox transformation correctly setup.
     * By the time the script starts running, this field is set to non-null.
     */
    private transient CpsGroovyShell shell;

    @Deprecated
    public CpsFlowExecution(String script, FlowExecutionOwner owner) throws IOException {
        this(script, false, owner);
    }

    CpsFlowExecution(String script, boolean sandbox, FlowExecutionOwner owner) throws IOException {
        this.owner = owner;
        this.script = script;
        this.sandbox = sandbox;
        this.storage = createStorage();
        Authentication auth = Jenkins.getAuthentication();
        this.user = auth.equals(ACL.SYSTEM) ? null : auth.getName();
    }

    /**
     * Perform post-deserialization state resurrection that handles version evolution
     */
    private Object readResolve() {
        if (loadedScripts==null)
            loadedScripts = new HashMap<String,String>();   // field added later
        return this;
    }

    /**
     * Returns a groovy compiler used to load the script.
     *
     * @see GroovyShell#getClassLoader()
     */
    public GroovyShell getShell() {
        return shell;
    }

    public FlowNodeStorage getStorage() {
        return storage;
    }

    /**
     * True if executing with groovy-sandbox, false if executing with approval.
     */
    public boolean isSandbox() {
        return sandbox;
    }

    @Override
    public FlowExecutionOwner getOwner() {
        return owner;
    }

    private SimpleXStreamFlowNodeStorage createStorage() throws IOException {
        return new SimpleXStreamFlowNodeStorage(this, getStorageDir());
    }

    /**
     * Directory where workflow stores its state.
     */
    public File getStorageDir() throws IOException {
        return new File(this.owner.getRootDir(),"workflow");
    }

    @Override
    public void start() throws IOException {
        final CpsScript s = parseScript();
        s.initialize();

        final FlowHead h = new FlowHead(this);
        synchronized (this) {
            heads.put(h.getId(), h);
        }
        h.newStartNode(new FlowStartNode(this, iotaStr()));

        final CpsThreadGroup g = new CpsThreadGroup(this);
        final SettableFuture<CpsThreadGroup> f = SettableFuture.create();
        g.runner.submit(new Runnable() {
            @Override
            public void run() {
                CpsThread t = g.addThread(new Continuable(s,createInitialEnv()),h,null);
                t.resume(new Outcome(null, null));
                f.set(g);
            }

            /**
             * Environment to start executing the script in.
             * During sandbox execution, we need to call sandbox interceptor while executing asynchronous code.
             */
            private Env createInitialEnv() {
                return Envs.empty( isSandbox() ? new SandboxInvoker() : new DefaultInvoker());
            }
        });

        programPromise = f;

    }

    private CpsScript parseScript() throws IOException {
        shell = new CpsGroovyShell(this);
        CpsScript s = (CpsScript) shell.reparse("WorkflowScript",script);

        for (Entry<String, String> e : loadedScripts.entrySet()) {
            shell.reparse(e.getKey(), e.getValue());
        }

        s.execution = this;
        if (false) {
            System.out.println("scriptName="+s.getClass().getName());
            System.out.println(Arrays.asList(s.getClass().getInterfaces()));
            System.out.println(Arrays.asList(s.getClass().getDeclaredFields()));
            System.out.println(Arrays.asList(s.getClass().getDeclaredMethods()));
        }
        return s;
    }

    /**
     * Assigns a new ID.
     */
    @Restricted(NoExternalUse.class)
    public String iotaStr() {
        return String.valueOf(iota());
    }

    @Restricted(NoExternalUse.class)
    public int iota() {
        return iota.incrementAndGet();
    }

    @Override
    public void onLoad() {
        try {
            if (!isComplete())
                loadProgramAsync(getProgramDataFile());
        } catch (IOException e) {
            SettableFuture<CpsThreadGroup> p = SettableFuture.create();
            programPromise = p;
            loadProgramFailed(e, p);
        }
    }

    /**
     * Deserializes {@link CpsThreadGroup} from {@link #getProgramDataFile()} if necessary.
     *
     * This moves us into the PREPARING state.
     * @param programDataFile
     */
    public void loadProgramAsync(File programDataFile) {
        final SettableFuture<CpsThreadGroup> result = SettableFuture.create();
        programPromise = result;

        try {
            final ClassLoader scriptClassLoader = parseScript().getClass().getClassLoader();

            RiverReader r = new RiverReader(programDataFile, scriptClassLoader, owner);
            Futures.addCallback(
                    r.restorePickles(),

                    new FutureCallback<Unmarshaller>() {
                        public void onSuccess(Unmarshaller u) {
                            CpsFlowExecution old = PROGRAM_STATE_SERIALIZATION.get();
                            PROGRAM_STATE_SERIALIZATION.set(CpsFlowExecution.this);
                            try {
                                CpsThreadGroup g = (CpsThreadGroup) u.readObject();
                                result.set(g);
                            } catch (Throwable t) {
                                onFailure(t);
                            } finally {
                                PROGRAM_STATE_SERIALIZATION.set(old);
                            }
                        }

                        public void onFailure(Throwable t) {
                            loadProgramFailed(t, result);
                        }
                    });

        } catch (IOException e) {
            loadProgramFailed(e, result);
        }
    }

    /**
     * Used by {@link #loadProgramAsync(File)} to propagate a failure to load the persisted execution state.
     * <p>
     * Let the workflow interrupt by throwing an exception that indicates how it failed.
     * @param promise same as {@link #programPromise} but more strongly typed
     */
    private void loadProgramFailed(final Throwable problem, SettableFuture<CpsThreadGroup> promise) {
        FlowHead head;

        synchronized(this) {
            if (heads.isEmpty())
                head = null;
            else
                head = getFirstHead();
        }

        if (head==null) {
            // something went catastrophically wrong and there's no live head. fake one
            head = new FlowHead(this);
            try {
                head.newStartNode(new FlowStartNode(this, iotaStr()));
            } catch (IOException e) {
                LOGGER.log(Level.FINE, "Failed to persist", e);
            }
        }


        CpsThreadGroup g = new CpsThreadGroup(this);
        final FlowHead head_ = head;

        promise.set(g);
        runInCpsVmThread(new FutureCallback<CpsThreadGroup>() {
            @Override public void onSuccess(CpsThreadGroup g) {
                CpsThread t = g.addThread(
                        new Continuable(new ThrowBlock(new ConstantBlock(
                            new IOException("Failed to load persisted workflow state", problem)))),
                        head_, null
                );
                t.resume(new Outcome(null,null));
            }
            @Override public void onFailure(Throwable t) {
                LOGGER.log(Level.WARNING, null, t);
            }
        });
    }

    /**
     * Where we store {@link CpsThreadGroup}.
     */
    /*package*/ File getProgramDataFile() throws IOException {
        return new File(owner.getRootDir(), "program.dat");
    }

    /**
     * Execute a task in {@link CpsVmExecutorService} to safely access {@link CpsThreadGroup} internal states.
     *
     * <p>
     * If the {@link CpsThreadGroup} deserializatoin fails, {@link FutureCallback#onFailure(Throwable)} will
     * be invoked (on a random thread, since {@link CpsVmThread} doesn't exist without a valid program.)
     */
    void runInCpsVmThread(final FutureCallback<CpsThreadGroup> callback) {
        // first we need to wait for programPromise to fullfil CpsThreadGroup, then we need to run in its runner, phew!
        Futures.addCallback(programPromise, new FutureCallback<CpsThreadGroup>() {
            final Exception source = new Exception();   // call stack of this object captures who called this. useful during debugging.
            @Override
            public void onSuccess(final CpsThreadGroup g) {
                g.runner.submit(new Runnable() {
                    @Override
                    public void run() {
                        callback.onSuccess(g);
                    }
                });
            }

            /**
             * Program state failed to load.
             */
            @Override
            public void onFailure(Throwable t) {
                callback.onFailure(t);
            }
        });
    }


    /**
     * Waits for the workflow to move into the SUSPENDED state.
     *
     * @throws Exception
     *      if the workflow has failed
     */
    public void waitForSuspension() throws InterruptedException, ExecutionException {
        if (programPromise==null)
            return; // the execution has already finished and we are not loading program state anymore
        CpsThreadGroup g = programPromise.get();
        // TODO occasionally tests fail here with RejectedExecutionException, apparently because the runner has been shut down; should we just ignore that?
        g.scheduleRun().get();
    }

    public synchronized FlowHead getFlowHead(int id) {
        return heads.get(id);
    }

    @Override
    public synchronized List<FlowNode> getCurrentHeads() {
        List<FlowNode> r = new ArrayList<FlowNode>();
        for (FlowHead h : heads.values()) {
            r.add(h.get());
        }
        return r;
    }

    @Override
    public ListenableFuture<List<StepExecution>> getCurrentExecutions() {
        if (programPromise==null)
            return Futures.immediateFuture(Collections.<StepExecution>emptyList());

        final SettableFuture<List<StepExecution>> r = SettableFuture.create();
        runInCpsVmThread(new FutureCallback<CpsThreadGroup>() {
            @Override
            public void onSuccess(CpsThreadGroup g) {
                // to exclude outer StepExecutions, first build a map by FlowHead
                // younger threads with their StepExecutions will overshadow old threads, leaving inner-most threads alone.
                Map<FlowHead,StepExecution> m = new HashMap<FlowHead, StepExecution>();
                for (CpsThread t : g.threads.values()) {
                    StepExecution e = t.getStep();
                    if (e!=null)
                        m.put(t.head,e);
                }

                r.set(ImmutableList.copyOf(m.values()));
            }

            @Override
            public void onFailure(Throwable t) {
                r.setException(t);
            }
        });

        return r;
    }

    @Override
    public synchronized boolean isCurrentHead(FlowNode n) {
        for (FlowHead h : heads.values()) {
            if (h.get().equals(n))
                return true;
        }
        return false;
    }

    // called by FlowHead to add a new head
    synchronized void addHead(FlowHead h) {
        heads.put(h.getId(), h);
    }

    synchronized void removeHead(FlowHead h) {
        heads.remove(h.getId());
    }


    @Override
    public void addListener(GraphListener listener) {
        if (listeners == null) {
            listeners = new CopyOnWriteArrayList<GraphListener>();
        }
        listeners.add(listener);
    }

    @Override
    public void interrupt(Result result, CauseOfInterruption... causes) throws IOException, InterruptedException {
        setResult(result);

        final FlowInterruptedException ex = new FlowInterruptedException(result,causes);

        // stop all ongoing activities
        Futures.addCallback(getCurrentExecutions(), new FutureCallback<List<StepExecution>>() {
            @Override
            public void onSuccess(List<StepExecution> l) {
                for (StepExecution e : Iterators.reverse(l)) {
                    try {
                        e.stop(ex);
                    } catch (Exception x) {
                        LOGGER.log(Level.WARNING, "Failed to abort " + CpsFlowExecution.this.toString(), x);
                    }
                }
            }

            @Override
            public void onFailure(Throwable t) {
            }
        });
    }

    @Override
    public FlowNode getNode(String id) throws IOException {
        return storage.getNode(id);
    }

    public void setResult(Result v) {
        result = result.combine(v);
    }

    public Result getResult() {
        return result;
    }

    public List<Action> loadActions(FlowNode node) throws IOException {
        return storage.loadActions(node);
    }

    public void saveActions(FlowNode node, List<Action> actions) throws IOException {
        storage.saveActions(node, actions);
    }

    @Override
    public boolean isComplete() {
        return done || super.isComplete();
    }

    /*packgage*/ synchronized void onProgramEnd(Outcome outcome) {
        // end of the program
        // run till the end successfully FIXME: failure comes here, too
        // TODO: if program terminates with exception, we need to record it
        // TODO: in the error case, we have to close all the open nodes
        FlowNode head = new FlowEndNode(this, iotaStr(), (FlowStartNode)startNodes.pop(), result, getCurrentHeads().toArray(new FlowNode[0]));
        if (outcome.isFailure())
            head.addAction(new ErrorAction(outcome.getAbnormal()));

        // shrink everything into a single new head
        done = true;
        FlowHead first = getFirstHead();
        first.setNewHead(head);
        heads.clear();
        heads.put(first.getId(),first);
    }

    synchronized FlowHead getFirstHead() {
        assert !heads.isEmpty();
        return heads.firstEntry().getValue();
    }

    void notifyListeners(FlowNode node) {
        if (listeners != null) {
            for (GraphListener listener : listeners) {
                listener.onNewHead(node);
            }
        }
    }

    @Override public Authentication getAuthentication() {
        if (user == null) {
            return ACL.SYSTEM;
        }
        try {
            return User.get(user).impersonate();
        } catch (UsernameNotFoundException x) {
            LOGGER.log(Level.WARNING, "could not restore authentication", x);
            // Should not expose this to callers.
            return Jenkins.ANONYMOUS;
        }
    }

    // TODO: write a custom XStream Converter so that while we are writing CpsFlowExecution, it holds that lock
    // the execution in Groovy CPS should hold that lock (or worse, hold that lock in the runNextChunk method)
    // so that the execution gets suspended while we are getting serialized

    public static final class ConverterImpl implements Converter {
        private final XStream xs;
        private final ReflectionProvider ref;
        private final Mapper mapper;

        public ConverterImpl(XStream xs) {
            this.xs = xs;
            this.ref = xs.getReflectionProvider();
            this.mapper = xs.getMapper();
        }

        public boolean canConvert(Class type) {
            return CpsFlowExecution.class==type;
        }

        public void marshal(Object source, HierarchicalStreamWriter w, MarshallingContext context) {
            CpsFlowExecution e = (CpsFlowExecution) source;

            writeChild(w, context, "result", e.result, Result.class);
            writeChild(w, context, "script", e.script, String.class);
            writeChild(w, context, "loadedScripts", e.loadedScripts, Map.class);
            writeChild(w, context, "owner", e.owner, Object.class);
            if (e.user != null) {
                writeChild(w, context, "user", e.user, String.class);
            }
            writeChild(w, context, "iota", e.iota.get(), Integer.class);
            synchronized (e) {
                for (FlowHead h : e.heads.values()) {
                    writeChild(w, context, "head", h.getId() + ":" + h.get().getId(), String.class);
                }
                for (BlockStartNode st : e.startNodes) {
                    writeChild(w, context, "start", st.getId(), String.class);
                }
            }
        }

        private <T> void writeChild(HierarchicalStreamWriter w, MarshallingContext context, String name, T v, Class<T> staticType) {
            if (!mapper.shouldSerializeMember(CpsFlowExecution.class,name))
                return;
            startNode(w, name, staticType);
            Class<?> actualType = v.getClass();
            if (actualType !=staticType)
                w.addAttribute(mapper.aliasForSystemAttribute("class"), mapper.serializedClass(actualType));

            context.convertAnother(v);
            w.endNode();
        }

        public Object unmarshal(HierarchicalStreamReader reader, final UnmarshallingContext context) {
            try {
                CpsFlowExecution result;
                if (context.currentObject()!=null) {
                    result = (CpsFlowExecution) context.currentObject();
                } else {
                    result = (CpsFlowExecution) ref.newInstance(CpsFlowExecution.class);
                }

                FlowNodeStorage storage = result.storage;
                Stack<BlockStartNode> startNodes = new Stack<BlockStartNode>();
                Map<Integer,FlowHead> heads = new TreeMap<Integer, FlowHead>();

                while (reader.hasMoreChildren()) {
                    reader.moveDown();

                    String nodeName = reader.getNodeName();
                    if (nodeName.equals("result")) {
                        Result r = readChild(reader, context, Result.class, result);
                        setField(result, "result", r);
                    } else
                    if (nodeName.equals("script")) {
                        String script = readChild(reader, context, String.class, result);
                        setField(result, "script", script);
                    } else
                    if (nodeName.equals("loadedScripts")) {
                        Map loadedScripts = readChild(reader, context, Map.class, result);
                        setField(result, "loadedScripts", loadedScripts);
                    } else
                    if (nodeName.equals("owner")) {
                        FlowExecutionOwner owner = (FlowExecutionOwner) readChild(reader, context, Object.class, result);
                        setField(result, "owner", owner);
                        setField(result, "storage", storage = result.createStorage());
                    } else
                    if (nodeName.equals("user")) {
                        String user = readChild(reader, context, String.class, result);
                        setField(result, "user", user);
                    } else
                    if (nodeName.equals("head")) {
                        String[] head = readChild(reader, context, String.class, result).split(":");
                        FlowHead h = new FlowHead(result, Integer.parseInt(head[0]));
                        h.setForDeserialize(storage.getNode(head[1]));
                        heads.put(h.getId(), h);
                    } else
                    if (nodeName.equals("iota")) {
                        Integer iota = readChild(reader, context, Integer.class, result);
                        setField(result, "iota", new AtomicInteger(iota));
                    } else
                    if (nodeName.equals("start")) {
                        String id = readChild(reader, context, String.class, result);
                        startNodes.add((BlockStartNode) storage.getNode(id));
                    }

                    reader.moveUp();
                }

                setField(result, "startNodes", startNodes);
                setField(result, "heads", heads);

                return result;
            } catch (IOException e) {
                throw new XStreamException("Failed to read back CpsFlowExecution",e);
            }
        }

        private void setField(CpsFlowExecution result, String fieldName, Object value) {
            ref.writeField(result, fieldName, value, CpsFlowExecution.class);
        }

        /**
         * Called when a reader is
         */
        private <T> T readChild(HierarchicalStreamReader r, UnmarshallingContext context, Class<T> type, Object parent) {
            String classAttribute = r.getAttribute(mapper.aliasForAttribute("class"));
            if (classAttribute != null) {
                type = mapper.realClass(classAttribute);
            }

            return type.cast(context.convertAnother(parent, type));
        }
    }

    private static final Logger LOGGER = Logger.getLogger(CpsFlowExecution.class.getName());

    /**
     * While we serialize/deserialize {@link CpsThreadGroup} and the entire program execution state,
     * this field is set to {@link CpsFlowExecution} that will own it.
     */
    static final ThreadLocal<CpsFlowExecution> PROGRAM_STATE_SERIALIZATION = new ThreadLocal<CpsFlowExecution>();
}
TOP

Related Classes of org.jenkinsci.plugins.workflow.cps.CpsFlowExecution$ConverterImpl

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.