/* * Copyright (c) 2013-2017 Red Hat, Inc. and/or its affiliates. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Cheng Fang - Initial API and implementation */ package org.jberet.job.model; import java.util.ArrayDeque; import java.util.Deque; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Properties; import javax.batch.operations.BatchRuntimeException; import org.jberet.job.model.Transition.End; import org.jberet.job.model.Transition.Fail; import org.jberet.job.model.Transition.Next; import org.jberet.job.model.Transition.Stop; import static org.jberet._private.BatchLogger.LOGGER; import static org.jberet._private.BatchMessages.MESSAGES; /** * Responsible for resolving property expressions in job and job elements. */ public final class PropertyResolver { protected static final String jobParametersToken = "jobParameters"; protected static final String jobPropertiesToken = "jobProperties"; protected static final String systemPropertiesToken = "systemProperties"; protected static final String partitionPlanToken = "partitionPlan"; private static final String prefix = "#{"; private static final String defaultValuePrefix = "?:"; private static final int shortestTemplateLen = "#{jobProperties['x']}".length(); private static final int prefixLen = prefix.length(); private Properties jobParameters; private Properties partitionPlanProperties; private final Deque<org.jberet.job.model.Properties> jobPropertiesStack = new ArrayDeque<org.jberet.job.model.Properties>(); private boolean resolvePartitionPlanProperties; /** * Sets job parameters to be used for resolving expressions referencing job parameters. * * @param jobParameters job parameters as {@code java.util.Properties} */ public void setJobParameters(final Properties jobParameters) { this.jobParameters = jobParameters; } /** * Sets partition plan properties to be used for resolving expressions referencing partition plan properties. * * @param partitionPlanProperties partition plan properties as {@code java.util.Properties} */ public void setPartitionPlanProperties(final Properties partitionPlanProperties) { this.partitionPlanProperties = partitionPlanProperties; } void pushJobProperties(final org.jberet.job.model.Properties jobProps) { this.jobPropertiesStack.push(jobProps); } /** * Sets the flag whether this class need to resolve partition plan properties or not. * Partition plan property substitution is resolved before the partition execution begins, which is later than * the substitution of job parameters, job properties and system properties. * * @param resolvePartitionPlanProperties whether to resolve partition plan or not */ public void setResolvePartitionPlanProperties(final boolean resolvePartitionPlanProperties) { this.resolvePartitionPlanProperties = resolvePartitionPlanProperties; } /** * Resolves property expressions for job-level elements contained in the job. * The job's direct job properties, job parameters and system properties should already have been set up properly, * e.g., when this instance was instantiated. * * @param job the job element whose properties need to be resolved */ public void resolve(final Job job) { final String oldVal; final String newVal; oldVal = job.getRestartable(); if (oldVal != null) { newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { job.setRestartable(newVal); } } final org.jberet.job.model.Properties props = job.getProperties(); //do not push or pop the top-level properties. They need to be sticky and may be referenced by lower-level props resolve(props, false); resolve(job.getListeners()); resolveJobElements(job.getJobElements()); if (props != null) { // the properties instance to pop is different from job.getProperties (a clone). jobPropertiesStack.pop(); } } /** * Resolves property expressions for a list of job elements. * * @param jobElements list of job elements */ private void resolveJobElements(final List<?> jobElements) { if (jobElements == null) { return; } for (final Object e : jobElements) { if (e instanceof Step) { resolve((Step) e); } else if (e instanceof Flow) { resolve((Flow) e); } else if (e instanceof Decision) { resolve((Decision) e); } else if (e instanceof Split) { resolve((Split) e); } } } /** * Resolves property expressions for step-level elements contained in the step. * The step's direct job properties, job parameters and system properties should already have been set up properly, * e.g., when this instance was instantiated. * * @param step the step element whose properties need to be resolved */ public void resolve(final Step step) { resolve(step.getPartition()); String oldVal, newVal; oldVal = step.getAttributeNext(); if (oldVal != null) { newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { step.setAttributeNext(newVal); } } oldVal = step.getAllowStartIfComplete(); if (oldVal != null) { newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { step.setAllowStartIfComplete(newVal); } } oldVal = step.getStartLimit(); if (oldVal != null) { newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { step.setStartLimit(newVal); } } final org.jberet.job.model.Properties props = step.getProperties(); resolve(props, false); resolve(step.getListeners()); final RefArtifact batchlet = step.getBatchlet(); if (batchlet != null) { oldVal = batchlet.getRef(); newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { batchlet.setRef(newVal); } resolve(batchlet.getProperties(), true); } resolve(step.getChunk()); resolveTransitionElements(step.getTransitionElements()); if (props != null) { jobPropertiesStack.pop(); } } private void resolve(final Partition partition) { if (partition == null) { return; } String oldVal, newVal; final RefArtifact analyzer = partition.getAnalyzer(); if (analyzer != null) { oldVal = analyzer.getRef(); newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { analyzer.setRef(newVal); } resolve(analyzer.getProperties(), true); } final RefArtifact collector = partition.getCollector(); if (collector != null) { oldVal = collector.getRef(); newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { collector.setRef(newVal); } resolve(collector.getProperties(), true); } final RefArtifact reducer = partition.getReducer(); if (reducer != null) { oldVal = reducer.getRef(); newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { reducer.setRef(newVal); } resolve(reducer.getProperties(), true); } final PartitionPlan plan = partition.getPlan(); if (plan != null) { oldVal = plan.getPartitions(); if (oldVal != null) { newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { plan.setPartitions(newVal); } } oldVal = plan.getThreads(); if (oldVal != null) { newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { plan.setThreads(newVal); } } //plan properties should be resolved at job load time (1st pass) if (!resolvePartitionPlanProperties) { for (final org.jberet.job.model.Properties p : plan.getPropertiesList()) { resolve(p, true); } } } final RefArtifact mapper = partition.getMapper(); if (mapper != null) { oldVal = mapper.getRef(); newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { mapper.setRef(newVal); } //mapper properties should be resolved at job load time (1st pass) if (!resolvePartitionPlanProperties) { resolve(mapper.getProperties(), true); } } } private void resolve(final Chunk chunk) { if (chunk == null) { return; } resolve(chunk.getSkippableExceptionClasses()); resolve(chunk.getRetryableExceptionClasses()); resolve(chunk.getNoRollbackExceptionClasses()); String oldVal, newVal; oldVal = chunk.getCheckpointPolicy(); if (oldVal != null) { newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { chunk.setCheckpointPolicy(newVal); } } oldVal = chunk.getItemCount(); if (oldVal != null) { newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { chunk.setItemCount(newVal); } } oldVal = chunk.getTimeLimit(); if (oldVal != null) { newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { chunk.setTimeLimit(newVal); } } oldVal = chunk.getSkipLimit(); if (oldVal != null) { newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { chunk.setSkipLimit(newVal); } } oldVal = chunk.getRetryLimit(); if (oldVal != null) { newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { chunk.setRetryLimit(newVal); } } final RefArtifact reader = chunk.getReader(); oldVal = reader.getRef(); newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { reader.setRef(newVal); } resolve(reader.getProperties(), true); final RefArtifact writer = chunk.getWriter(); oldVal = writer.getRef(); newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { writer.setRef(newVal); } resolve(writer.getProperties(), true); final RefArtifact processor = chunk.getProcessor(); if (processor != null) { oldVal = processor.getRef(); newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { processor.setRef(newVal); } resolve(processor.getProperties(), true); } final RefArtifact checkpointAlgorithm = chunk.getCheckpointAlgorithm(); if (checkpointAlgorithm != null) { oldVal = checkpointAlgorithm.getRef(); if (oldVal != null) { //TODO remove the if, ref attr should be required newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { checkpointAlgorithm.setRef(newVal); } } resolve(checkpointAlgorithm.getProperties(), true); } } private void resolve(final ExceptionClassFilter filter) { if (filter == null) { return; } resolveIncludeOrExclude(filter.include); resolveIncludeOrExclude(filter.exclude); } private void resolveIncludeOrExclude(final List<String> clude) { for (final ListIterator<String> it = clude.listIterator(); it.hasNext();) { final String oldVal = it.next(); final String newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { it.set(newVal); } } } private void resolve(final Split split) { final String oldVal = split.getAttributeNext(); if (oldVal != null) { final String newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { split.setAttributeNext(newVal); } } for (final Flow e : split.getFlows()) { resolve(e); } } private void resolve(final Flow flow) { final String oldVal = flow.getAttributeNext(); if (oldVal != null) { final String newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { flow.next = newVal; } } resolveTransitionElements(flow.getTransitionElements()); resolveJobElements(flow.jobElements); } private void resolve(final org.jberet.job.model.Properties props, final boolean popProps) { if (props == null) { return; } //push individual property to resolver one by one, so only those properties that are already defined can be //visible to the resolver. final org.jberet.job.model.Properties propsToPush = new org.jberet.job.model.Properties(); jobPropertiesStack.push(propsToPush); try { final String oldPartitionVal = props.getPartition(); if (oldPartitionVal != null) { final String newPartitionVal = resolve(oldPartitionVal); if (!oldPartitionVal.equals(newPartitionVal)) { props.setPartition(newPartitionVal); } } final Map<String, String> propertiesMapping = props.getPropertiesMapping(); for (final Map.Entry<String, String> entry : propertiesMapping.entrySet()) { final String oldKey = entry.getKey(); final String newKey = resolve(oldKey); final String oldVal = entry.getValue(); final String newVal = resolve(oldVal); if (oldKey.equals(newKey)) { if (!oldVal.equals(newVal)) { props.add(newKey, newVal); } } else { props.remove(oldKey); props.add(newKey, newVal); } propsToPush.add(newKey, newVal); } } finally { if (popProps) { jobPropertiesStack.pop(); } } } private void resolve(final Listeners listeners) { if (listeners == null) { return; } for (final RefArtifact l : listeners.getListeners()) { final String oldVal = l.getRef(); final String newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { l.setRef(newVal); } resolve(l.getProperties(), true); } } private void resolve(final Decision decision) { final String oldVal = decision.getRef(); final String newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { decision.setRef(newVal); } resolve(decision.getProperties(), true); resolveTransitionElements(decision.getTransitionElements()); } private void resolveTransitionElements(final List<?> transitions) { String oldVal, newVal; for (final Object e : transitions) { if (e instanceof Next) { final Next next = (Next) e; oldVal = next.getTo(); newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { next.setTo(newVal); } oldVal = next.getOn(); newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { next.setOn(newVal); } } else if (e instanceof Fail) { final Fail fail = (Fail) e; oldVal = fail.getOn(); newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { fail.setOn(newVal); } oldVal = fail.getExitStatus(); if (oldVal != null) { newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { fail.setExitStatus(newVal); } } } else if (e instanceof End) { final End end = (End) e; oldVal = end.getOn(); newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { end.setOn(newVal); } oldVal = end.getExitStatus(); if (oldVal != null) { newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { end.setExitStatus(newVal); } } } else if (e instanceof Stop) { final Stop stop = (Stop) e; oldVal = stop.getOn(); newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { stop.setOn(newVal); } oldVal = stop.getExitStatus(); if (oldVal != null) { newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { stop.setExitStatus(newVal); } } oldVal = stop.getRestart(); if (oldVal != null) { newVal = resolve(oldVal); if (!oldVal.equals(newVal)) { stop.setRestart(newVal); } } } } } /** * Resolves an expression from the raw value. * * @param rawVale the raw value * * @return the resolved string or {@code null} if unresolvable * * @throws java.lang.SecurityException if a security manager is installed, {@code systemProperties} is requested * and the system property permission is not set. */ String resolve(final String rawVale) { if (rawVale.length() < shortestTemplateLen || !rawVale.contains(prefix)) { return rawVale; } final StringBuilder sb = new StringBuilder(rawVale); try { resolve(sb, 0, true, null); } catch (final BatchRuntimeException e) { LOGGER.unresolvableExpression(e.getMessage()); return null; } return sb.toString(); } /** * Resolves a JSL batch property value that may contain a JSL expression. * * @param sb the batch property value as StringBuilder * @param start starting position to track recursive calls of this method * @param defaultAllowed whether there is a default value * @param referringExpressions referring expression for tracking cyclic references * * @throws BatchRuntimeException if there is a cyclic reference * @throws IllegalArgumentException for invalid JSL property expressions such as missing ', [, or ] */ private void resolve(final StringBuilder sb, final int start, final boolean defaultAllowed, final LinkedList<String> referringExpressions) throws BatchRuntimeException, IllegalArgumentException { //distance-to-end doesn't have space for any template, so no variable referenced if (sb.length() - start < shortestTemplateLen) { return; } final int startExpression = sb.indexOf(prefix, start); if (startExpression < 0) { //doesn't reference any variable return; } final int startPropCategory = startExpression + prefixLen; final int openBracket = sb.indexOf("[", startPropCategory); if (openBracket < 0) { throw MESSAGES.invalidPropertyExpression(sb.toString()); } final char startQuote = sb.charAt(openBracket + 1); if (startQuote != '\'' && startQuote != '"') { throw MESSAGES.invalidPropertyExpression(sb.toString()); } final String propCategory = sb.substring(startPropCategory, openBracket); final int startVariableName = openBracket + 2; //jump to the next char after ', the start of variable name final int endBracket = sb.indexOf("]", startVariableName + 1); if (endBracket < 1) { throw MESSAGES.invalidPropertyExpression(sb.toString()); } final char endQuote = sb.charAt(endBracket - 1); if (endQuote != '\'' && endQuote != '"') { throw MESSAGES.invalidPropertyExpression(sb.toString()); } int endExpression = endBracket + 1; if (endExpression >= sb.length()) { //this can happen when missing an ending } (e.g., #{jobProperties['step-prop'] ) LOGGER.possibleSyntaxErrorInProperty(sb.toString()); endExpression = sb.length() - 1; } int endCurrentPass = endExpression; final String expression = sb.substring(startExpression, endExpression + 1); if (referringExpressions != null && referringExpressions.contains(expression)) { throw MESSAGES.cycleInPropertyReference(referringExpressions); } if (!resolvePartitionPlanProperties && propCategory.equals(partitionPlanToken)) { resolve(sb, endCurrentPass + 1, true, null); return; } final String variableName = sb.substring(startVariableName, endBracket - 1); // ['abc'] String val = getPropertyValue(variableName, propCategory, sb); if (val != null) { val = reresolve(expression, val, defaultAllowed, referringExpressions); } if (!defaultAllowed) { //a default expression should not have default again if (val == null) { //not resolved: //in xml space, unresolved properties is set to "",for example, a#{jobProperties['no.such.prop']}b => ab //when injecting unresolved properties to artifact class, null is injected for property value "". //throw LOGGER.unresolvableExpressionException(sb.toString()); val = ""; } endCurrentPass = replaceAndGetEndPosition(sb, startExpression, endExpression, val); } else { final int startDefaultMarker = endExpression + 1; final int endDefaultMarker = startDefaultMarker + 1; //?: String next2Chars = null; if (endDefaultMarker >= sb.length()) { //no default value expression } else { next2Chars = sb.substring(startDefaultMarker, endDefaultMarker + 1); } final boolean hasDefault = defaultValuePrefix.equals(next2Chars); int endDefaultExpressionMarker = sb.indexOf(";", endDefaultMarker + 1); if (endDefaultExpressionMarker < 0) { endDefaultExpressionMarker = sb.length(); } if (val != null) { if (!hasDefault) { //resolved, no default: replace the expression with value endCurrentPass = replaceAndGetEndPosition(sb, startExpression, endExpression, val); } else { //resolved, has default: replace the expression and the default expression with value endCurrentPass = replaceAndGetEndPosition(sb, startExpression, endDefaultExpressionMarker, val); } } else { if (!hasDefault) { //not resolved, no default: //throw LOGGER.unresolvableExpressionException(sb.toString()); endCurrentPass = replaceAndGetEndPosition(sb, startExpression, endExpression, ""); } else { //not resolved, has default: resolve and apply the default final StringBuilder sb4DefaultExpression = new StringBuilder(sb.substring(endDefaultMarker + 1, endDefaultExpressionMarker)); resolve(sb4DefaultExpression, 0, false, null); endCurrentPass = replaceAndGetEndPosition(sb, startExpression, endDefaultExpressionMarker, sb4DefaultExpression.toString()); } } } resolve(sb, endCurrentPass + 1, true, null); } private String reresolve(final String expression, final String currentlyResolvedToVal, final boolean defaultAllowed, LinkedList<String> referringExpressions) throws BatchRuntimeException { if (currentlyResolvedToVal.length() < shortestTemplateLen || !currentlyResolvedToVal.contains(prefix)) { return currentlyResolvedToVal; } if (referringExpressions == null) { referringExpressions = new LinkedList<String>(); } referringExpressions.add(expression); final StringBuilder sb = new StringBuilder(currentlyResolvedToVal); try { resolve(sb, 0, defaultAllowed, referringExpressions); } catch (final BatchRuntimeException e) { LOGGER.unresolvableExpression(e.getMessage()); return null; } return sb.toString(); } private int replaceAndGetEndPosition(final StringBuilder sb, final int startExpression, final int endExpression, final String replacingVal) { sb.replace(startExpression, endExpression + 1, replacingVal); return startExpression - 1 + replacingVal.length(); } private String getPropertyValue(final String variableName, final String propCategory, final StringBuilder sb) { String val = null; if (propCategory.equals(jobParametersToken)) { if (jobParameters != null) { val = jobParameters.getProperty(variableName); } } else if (propCategory.equals(jobPropertiesToken)) { for (final org.jberet.job.model.Properties p : jobPropertiesStack) { val = p.get(variableName); if (val != null) { break; } } } else if (propCategory.equals(systemPropertiesToken)) { if (variableName == null || variableName.isEmpty()) { val = null; } else { val = System.getProperty(variableName); } } else if (propCategory.equals(partitionPlanToken)) { if (partitionPlanProperties != null) { val = partitionPlanProperties.getProperty(variableName); } } else { LOGGER.unrecognizedPropertyReference(propCategory, variableName, sb.toString()); } return val; } }