Java tutorial
/* * 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.Outcome; import groovy.lang.Closure; import groovy.lang.GString; import groovy.lang.GroovyObject; import groovy.lang.GroovyObjectSupport; import groovy.lang.GroovyRuntimeException; import hudson.EnvVars; import hudson.model.Computer; import hudson.model.Describable; import hudson.model.Descriptor; import hudson.model.Queue; import hudson.model.Run; import hudson.model.TaskListener; import java.io.IOException; import java.io.PrintStream; import java.io.Serializable; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.model.Jenkins; import org.apache.commons.lang.StringUtils; import org.codehaus.groovy.reflection.CachedClass; import org.codehaus.groovy.reflection.ReflectionCache; import org.jenkinsci.Symbol; import org.jenkinsci.plugins.structs.SymbolLookup; import org.jenkinsci.plugins.structs.describable.DescribableModel; import org.jenkinsci.plugins.structs.describable.DescribableParameter; import org.jenkinsci.plugins.structs.describable.UninstantiatedDescribable; import static org.jenkinsci.plugins.workflow.cps.ThreadTaskResult.*; import org.jenkinsci.plugins.workflow.cps.actions.ArgumentsActionImpl; import org.jenkinsci.plugins.workflow.cps.nodes.StepAtomNode; import org.jenkinsci.plugins.workflow.cps.nodes.StepEndNode; import org.jenkinsci.plugins.workflow.cps.nodes.StepStartNode; import org.jenkinsci.plugins.workflow.cps.persistence.PersistIn; import static org.jenkinsci.plugins.workflow.cps.persistence.PersistenceContext.*; import org.jenkinsci.plugins.workflow.cps.steps.LoadStep; import org.jenkinsci.plugins.workflow.cps.steps.ParallelStep; import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner; import org.jenkinsci.plugins.workflow.graph.FlowNode; import org.jenkinsci.plugins.workflow.steps.BodyExecutionCallback; import org.jenkinsci.plugins.workflow.steps.MissingContextVariableException; import org.jenkinsci.plugins.workflow.steps.Step; import org.jenkinsci.plugins.workflow.steps.StepContext; import org.jenkinsci.plugins.workflow.steps.StepDescriptor; import org.jenkinsci.plugins.workflow.steps.StepExecution; import org.jvnet.hudson.annotation_indexer.Index; import org.kohsuke.stapler.ClassDescriptor; import org.kohsuke.stapler.NoStaplerConstructorException; /** * Calls {@link Step}s and other DSL objects. */ @PersistIn(PROGRAM) public class DSL extends GroovyObjectSupport implements Serializable { private final FlowExecutionOwner handle; private transient CpsFlowExecution exec; private transient Map<String, StepDescriptor> functions; private static final Logger LOGGER = Logger.getLogger(DSL.class.getName()); public DSL(FlowExecutionOwner handle) { this.handle = handle; } protected Object readResolve() throws IOException { return this; } private static final String KEEP_STEP_ARGUMENTS_PROPERTYNAME = (DSL.class.getName() + ".keepStepArguments"); private static boolean isKeepStepArguments = StringUtils .isEmpty(System.getProperty(KEEP_STEP_ARGUMENTS_PROPERTYNAME)) || Boolean.parseBoolean(System.getProperty(KEEP_STEP_ARGUMENTS_PROPERTYNAME)); /** Tell us if we should store {@link Step} arguments in an {@link org.jenkinsci.plugins.workflow.actions.ArgumentsAction} * or simply discard them (if set to false, explicitly) */ public static boolean isKeepStepArguments() { return isKeepStepArguments; } /** * Executes the {@link Step} implementation specified by the name argument. * * @return * If the step completes execution synchronously, the result will be * returned. Otherwise this method {@linkplain Continuable#suspend(Object) suspends}. */ @Override @CpsVmThreadOnly public Object invokeMethod(String name, Object args) { try { if (exec == null) exec = (CpsFlowExecution) handle.get(); } catch (IOException e) { throw new GroovyRuntimeException(e); } if (functions == null) { functions = new TreeMap<>(); while (StepDescriptor.all().isEmpty()) { LOGGER.warning("Jenkins does not seem to be fully started yet, waiting"); try { Thread.sleep(1000); } catch (InterruptedException x) { throw new GroovyRuntimeException(x); } } for (StepDescriptor d : StepDescriptor.all()) { functions.put(d.getFunctionName(), d); } } final StepDescriptor sd = functions.get(name); if (sd != null) { return invokeStep(sd, args); } if (SymbolLookup.get().findDescriptor(Describable.class, name) != null) { return invokeDescribable(name, args); } Set<String> symbols = new TreeSet<>(); Set<String> globals = new TreeSet<>(); // TODO SymbolLookup only lets us find a particular symbol, not enumerate them try { for (Class<?> e : Index.list(Symbol.class, Jenkins.getActiveInstance().pluginManager.uberClassLoader, Class.class)) { if (Descriptor.class.isAssignableFrom(e)) { symbols.addAll(SymbolLookup.getSymbolValue(e)); } } Queue.Executable executable = exec.getOwner().getExecutable(); for (GlobalVariable var : GlobalVariable.forRun(executable instanceof Run ? (Run) executable : null)) { globals.add(var.getName()); } } catch (IOException x) { Logger.getLogger(DSL.class.getName()).log(Level.WARNING, null, x); } // TODO probably this should be throwing a subtype of groovy.lang.MissingMethodException throw new NoSuchMethodError("No such DSL method '" + name + "' found among steps " + functions.keySet() + " or symbols " + symbols + " or globals " + globals); } /** * When {@link #invokeMethod(String, Object)} is calling a {@link StepDescriptor} */ protected Object invokeStep(StepDescriptor d, Object args) { final NamedArgsAndClosure ps = parseArgs(args, d); CpsThread thread = CpsThread.current(); FlowNode an; // TODO: generalize the notion of Step taking over the FlowNode creation. boolean hack = d instanceof ParallelStep.DescriptorImpl || d instanceof LoadStep.DescriptorImpl; if (ps.body == null && !hack) { an = new StepAtomNode(exec, d, thread.head.get()); // TODO: use CPS call stack to obtain the current call site source location. See JENKINS-23013 thread.head.setNewHead(an); } else { an = new StepStartNode(exec, d, thread.head.get()); thread.head.setNewHead(an); } final CpsStepContext context = new CpsStepContext(d, thread, handle, an, ps.body); Step s; boolean sync; ClassLoader originalLoader = Thread.currentThread().getContextClassLoader(); try { d.checkContextAvailability(context); Thread.currentThread().setContextClassLoader(CpsVmExecutorService.ORIGINAL_CONTEXT_CLASS_LOADER.get()); s = d.newInstance(ps.namedArgs); try { // No point storing empty arguments, and ParallelStep is a special case where we can't store its closure arguments if (ps.namedArgs != null && !(ps.namedArgs.isEmpty()) && isKeepStepArguments() && !(s instanceof ParallelStep)) { // Get the environment variables to find ones that might be credentials bindings Computer comp = context.get(Computer.class); EnvVars allEnv = new EnvVars(context.get(EnvVars.class)); if (comp != null && allEnv != null) { allEnv.entrySet().removeAll(comp.getEnvironment().entrySet()); } an.addAction(new ArgumentsActionImpl(ps.namedArgs, allEnv)); } } catch (Exception e) { // Avoid breaking execution because we can't store some sort of crazy Step argument LOGGER.log(Level.WARNING, "Error storing the arguments for step: " + d.getFunctionName(), e); } StepExecution e = s.start(context); thread.setStep(e); sync = e.start(); } catch (Exception e) { if (e instanceof MissingContextVariableException) reportMissingContextVariableException(context, (MissingContextVariableException) e); context.onFailure(e); s = null; sync = true; } finally { Thread.currentThread().setContextClassLoader(originalLoader); } if (sync) { assert context.bodyInvokers .isEmpty() : "If a step claims synchronous completion, it shouldn't invoke body"; if (context.getOutcome() == null) { context.onFailure(new AssertionError("Step " + s + " claimed to have ended synchronously, but didn't set the result via StepContext.onSuccess/onFailure")); } thread.setStep(null); // if the execution has finished synchronously inside the start method // we just move on accordingly if (an instanceof StepStartNode) { // no body invoked, so EndNode follows StartNode immediately. thread.head.setNewHead(new StepEndNode(exec, (StepStartNode) an, an)); } thread.head.markIfFail(context.getOutcome()); return context.replay(); } else { // if it's in progress, suspend it until we get invoked later. // when it resumes, the CPS caller behaves as if this method returned with the resume value Continuable.suspend(new ThreadTaskImpl(context)); // the above method throws an exception to unwind the call stack, and // the control then goes back to CpsFlowExecution.runNextChunk // so the execution will never reach here. throw new AssertionError(); } } private static String loadSoleArgumentKey(StepDescriptor d) { try { String[] names = new ClassDescriptor(d.clazz).loadConstructorParamNames(); return names.length == 1 ? names[0] : null; } catch (NoStaplerConstructorException e) { return null; } } /** * When {@link #invokeMethod(String, Object)} is calling a generic {@link Descriptor} */ protected Object invokeDescribable(String symbol, Object _args) { List<StepDescriptor> metaSteps = StepDescriptor.metaStepsOf(symbol); StepDescriptor metaStep = metaSteps.size() == 1 ? metaSteps.get(0) : null; boolean singleArgumentOnly = false; if (metaStep != null) { Descriptor symbolDescriptor = SymbolLookup.get().findDescriptor(metaStep.getMetaStepArgumentType(), symbol); DescribableModel<?> symbolModel = new DescribableModel(symbolDescriptor.clazz); singleArgumentOnly = symbolModel.hasSingleRequiredParameter() && symbolModel.getParameters().size() == 1; } // The only time a closure is valid is when the resulting Describable is immediately executed via a meta-step NamedArgsAndClosure args = parseArgs(_args, metaStep != null && metaStep.takesImplicitBlockArgument(), UninstantiatedDescribable.ANONYMOUS_KEY, singleArgumentOnly); UninstantiatedDescribable ud = new UninstantiatedDescribable(symbol, null, args.namedArgs); if (metaStep == null) { // there's no meta-step associated with it, so this symbol is not executable. // in this case we assume this is building a nested object used as an eventual // parameter of an executable symbol, e.g., // // hg source: 'https://whatever/', clean: true, browser: kallithea('https://whatever/') // also note that in this case 'd' is not trustworthy, as depending on // where this UninstantiatedDescribable is ultimately used, the symbol // might be resolved with a specific type. return ud; } else { Descriptor d = SymbolLookup.get().findDescriptor(metaStep.getMetaStepArgumentType(), symbol); try { // execute this Describable through a meta-step // split args between MetaStep (represented by mm) and Describable (represented by dm) DescribableModel<?> mm = new DescribableModel(metaStep.clazz); DescribableModel<?> dm = new DescribableModel(d.clazz); DescribableParameter p = mm.getFirstRequiredParameter(); if (p == null) { // meta-step not having a required parameter is a bug in this meta step throw new IllegalArgumentException( "Attempted to use meta-step " + metaStep.getFunctionName() + " to process " + symbol + " but this meta-step is buggy; it has no mandatory parameter"); } // order of preference: // 1. mandatory parameter in mm // 2. mandatory parameter in dm // 3. other parameters in mm // 4. other parameters in dm // mm is preferred over dm because that way at least the arguments that mm defines // act consistently Map<String, Object> margs = new TreeMap<>(); Map<String, Object> dargs = new TreeMap<>(); for (Entry<String, ?> e : ud.getArguments().entrySet()) { String n = e.getKey(); Object v = e.getValue(); DescribableParameter mp = mm.getParameter(n); DescribableParameter dp = dm.getParameter(n); if (mp != null && mp.isRequired()) { margs.put(n, v); } else if (dp != null && dp.isRequired()) { dargs.put(n, v); } else if (mp != null) { margs.put(n, v); } else { // dp might be null, but this error will be caught by UD.instantiate() later dargs.put(n, v); } } ud = new UninstantiatedDescribable(symbol, null, dargs); margs.put(p.getName(), ud); return invokeStep(metaStep, new NamedArgsAndClosure(margs, args.body)); } catch (Exception e) { throw new IllegalArgumentException("Failed to prepare " + symbol + " step", e); } } } /** * Reports a user-friendly error message for {@link MissingContextVariableException}. */ private void reportMissingContextVariableException(CpsStepContext context, MissingContextVariableException e) { TaskListener tl; try { tl = context.get(TaskListener.class); if (tl == null) return; // if we can't report an error, give up } catch (IOException _) { return; } catch (InterruptedException _) { return; } StringBuilder names = new StringBuilder(); for (StepDescriptor p : e.getProviders()) { if (names.length() > 0) names.append(','); names.append(p.getFunctionName()); } PrintStream logger = tl.getLogger(); logger.println(e.getMessage()); if (names.length() > 0) logger.println( "Perhaps you forgot to surround the code with a step that provides this, such as: " + names); } static class NamedArgsAndClosure { final Map<String, Object> namedArgs; final Closure body; private NamedArgsAndClosure(Map<?, ?> namedArgs, Closure body) { this.namedArgs = new LinkedHashMap<String, Object>(); this.body = body; for (Map.Entry<?, ?> entry : namedArgs.entrySet()) { String k = entry.getKey().toString(); // coerces GString and more Object v = flattenGString(entry.getValue()); this.namedArgs.put(k, v); } } } /** * Coerce {@link GString}, to save {@link StepDescriptor#newInstance(Map)} from being made aware of that. * This is not the only type coercion that Groovy does, so this is not very kosher, but * doing a proper coercion like Groovy does require us to know the type that the receiver * expects. * For reference, Groovy does {@linkplain ReflectionCache#getCachedClass ReflectionCache.getCachedClass(types[i]).}{@linkplain CachedClass#coerceArgument coerceArgument(a)}. * Note that {@link DescribableModel#instantiate} would also handle {@link GString} in {@code coerce}, * but better to do it here in the Groovy-specific code so we do not need to rely on that. * @return {@code v} or an equivalent with all {@link GString}s flattened, including in nested {@link List}s or {@link Map}s */ private static Object flattenGString(Object v) { if (v instanceof GString) { return v.toString(); } else if (v instanceof List) { boolean mutated = false; List<Object> r = new ArrayList<>(); for (Object o : ((List<?>) v)) { Object o2 = flattenGString(o); mutated |= o != o2; r.add(o2); } return mutated ? r : v; } else if (v instanceof Map) { boolean mutated = false; Map<Object, Object> r = new LinkedHashMap<>(); for (Map.Entry<?, ?> e : ((Map<?, ?>) v).entrySet()) { Object k = e.getKey(); Object k2 = flattenGString(k); Object o = e.getValue(); Object o2 = flattenGString(o); mutated |= k != k2 || o != o2; r.put(k2, o2); } return mutated ? r : v; } else { return v; } } static NamedArgsAndClosure parseArgs(Object arg, StepDescriptor d) { boolean singleArgumentOnly = false; try { DescribableModel<?> stepModel = new DescribableModel<>(d.clazz); singleArgumentOnly = stepModel.hasSingleRequiredParameter() && stepModel.getParameters().size() == 1; } catch (NoStaplerConstructorException e) { // Ignore steps without databound constructors and treat them as normal. } return parseArgs(arg, d.takesImplicitBlockArgument(), loadSoleArgumentKey(d), singleArgumentOnly); } /** * Given the Groovy style argument packing used in the sole object parameter of {@link GroovyObject#invokeMethod(String, Object)}, * compute the named argument map and an optional closure that represents the body. * * <p> * Positional arguments are not allowed, unless it has a single argument, in which case * it is passed as an argument named "value", that is: * * <pre> * foo(x) => foo(value:x) * </pre> * * <p> * This handling is designed after how Java defines literal syntax for {@link Annotation}. * * @param arg * Argument object of {@link GroovyObject#invokeMethod(String, Object)} * @param expectsBlock * If a closure is a valid possible argument. If false and we see a block, this method throws an exception. * @param soleArgumentKey * If the context in which this method call happens allow implicit sole default argument, specify its name. * If null, the call must be with names arguments. */ static NamedArgsAndClosure parseArgs(Object arg, boolean expectsBlock, String soleArgumentKey, boolean singleRequiredArg) { if (arg instanceof NamedArgsAndClosure) return (NamedArgsAndClosure) arg; if (arg instanceof Map) // TODO is this clause actually used? return new NamedArgsAndClosure((Map) arg, null); if (arg instanceof Closure && expectsBlock) return new NamedArgsAndClosure(Collections.<String, Object>emptyMap(), (Closure) arg); if (arg instanceof Object[]) {// this is how Groovy appears to pack argument list into one Object for invokeMethod List a = Arrays.asList((Object[]) arg); if (a.size() == 0) return new NamedArgsAndClosure(Collections.<String, Object>emptyMap(), null); Closure c = null; Object last = a.get(a.size() - 1); if (last instanceof Closure && expectsBlock) { c = (Closure) last; a = a.subList(0, a.size() - 1); } if (a.size() == 1 && a.get(0) instanceof Map && !((Map) a.get(0)).containsKey("$class")) { Map mapArg = (Map) a.get(0); if (!singleRequiredArg || (soleArgumentKey != null && mapArg.size() == 1 && mapArg.containsKey(soleArgumentKey))) { // this is how Groovy passes in Map return new NamedArgsAndClosure(mapArg, c); } } switch (a.size()) { case 0: return new NamedArgsAndClosure(Collections.<String, Object>emptyMap(), c); case 1: return new NamedArgsAndClosure(singleParam(soleArgumentKey, a.get(0)), c); default: throw new IllegalArgumentException("Expected named arguments but got " + a); } } return new NamedArgsAndClosure(singleParam(soleArgumentKey, arg), null); } private static Map<String, Object> singleParam(String soleArgumentKey, Object arg) { if (soleArgumentKey != null) { return Collections.singletonMap(soleArgumentKey, arg); } else { throw new IllegalArgumentException("Expected named arguments but got " + arg); } } /** * If the step starts executing asynchronously, this task * executes at the safe point to switch {@link CpsStepContext} into the async mode. */ private static class ThreadTaskImpl extends ThreadTask implements Serializable { private final CpsStepContext context; public ThreadTaskImpl(CpsStepContext context) { this.context = context; } @Override protected ThreadTaskResult eval(CpsThread cur) { invokeBody(cur); if (!context.switchToAsyncMode()) { // we have a result now, so just keep executing // TODO: if this fails with an exception, we need ability to resume by throwing an exception return resumeWith(context.getOutcome()); } else { // beyond this point, StepContext can receive a result at any time and // that would result in a call to scheduleNextChunk(). So we the call to // switchToAsyncMode to happen inside 'synchronized(lock)', so that // the 'executing' variable gets set to null before the scheduleNextChunk call starts going. return suspendWith(new Outcome(context, null)); } } private void invokeBody(CpsThread cur) { // prepare enough heads for all the bodies // the first one can reuse the current thread, but other ones need to create new heads // we want to do this first before starting body so that the order of heads preserve // natural ordering. FlowHead[] heads = new FlowHead[context.bodyInvokers.size()]; for (int i = 0; i < heads.length; i++) { heads[i] = i == 0 ? cur.head : cur.head.fork(); } int idx = 0; for (CpsBodyInvoker b : context.bodyInvokers) { // don't collect the first head, which is what we borrowed from our parent. FlowHead h = heads[idx]; b.launch(cur, h); context.bodyHeads.add(h.getId()); idx++; } context.bodyInvokers.clear(); } /** * When a new {@link CpsThread} that runs the body completes, record * its new head. * * @deprecated * Unused as of 1.2. Left here for serialization compatibility. */ private static class HeadCollector extends BodyExecutionCallback { private final CpsStepContext context; private final FlowHead head; public HeadCollector(CpsStepContext context, FlowHead head) { this.context = context; this.head = head; } private void onEnd() { } @Override public void onSuccess(StepContext context, Object result) { onEnd(); } @Override public void onFailure(StepContext context, Throwable t) { onEnd(); } } private static final long serialVersionUID = 1L; } private static final long serialVersionUID = 1L; }