/* * Copyright (c) 2017 OBiBa. All rights reserved. * * This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.obiba.magma.js.validation; import java.io.Serializable; import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; import javax.validation.constraints.NotNull; import org.mozilla.javascript.CompilerEnvirons; import org.mozilla.javascript.Parser; import org.mozilla.javascript.ast.AstRoot; import org.obiba.magma.AttributeAware; import org.obiba.magma.MagmaEngine; import org.obiba.magma.ValueTable; import org.obiba.magma.Variable; import org.obiba.magma.js.MagmaJsEvaluationRuntimeException; import org.obiba.magma.support.MagmaEngineVariableResolver; import org.obiba.magma.support.ValueTableWrapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.base.Stopwatch; import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import static org.obiba.magma.js.JavascriptVariableBuilder.SCRIPT_ATTRIBUTE_NAME; @SuppressWarnings("ConstantNamingConvention") public class VariableScriptValidator { private static final Logger log = LoggerFactory.getLogger(VariableScriptValidator.class); private static final Pattern $_CALL = Pattern.compile("\\$\\(['\"](([\\d\\w.:\\-_]*))['\"]\\)"); private static final Pattern $THIS_CALL = Pattern.compile("\\$this\\(['\"](([\\d\\w.:\\-_]*))['\"]\\)"); private static final Pattern $VAR_CALL = Pattern.compile("\\$var\\(['\"](([\\d\\w.:\\-_]*))['\"]\\)"); // private static final Pattern $JOIN_CALL = Pattern.compile("(\\$join\\((['\"](([\\d\\w.:]*))['\"])*\\))"); private static final CompilerEnvirons COMPILER_ENVIRONS = new CompilerEnvirons(); static { COMPILER_ENVIRONS.setRecordingLocalJsDocComments(false); COMPILER_ENVIRONS.setAllowSharpComments(false); COMPILER_ENVIRONS.setRecordingComments(false); } @NotNull private final Variable variable; @NotNull private final ValueTable table; public VariableScriptValidator(@NotNull Variable variable, @NotNull ValueTable table) { //noinspection ConstantConditions Preconditions.checkArgument(table != null, "Cannot validate script with null table/view for " + variable.getName()); this.variable = variable; this.table = table; } public void validateScript() throws CircularVariableDependencyException { Stopwatch stopwatch = Stopwatch.createStarted(); getVariableRefNode(new VariableRefNode(variable.getVariableReference(table), table, getScript(variable))); log.debug("Script validation of {} in {}", variable.getName(), stopwatch); } private static void getVariableRefNode(@NotNull VariableRefNode callerNode) { String script = callerNode.getScript(); if(Strings.isNullOrEmpty(script)) { log.trace("{} has no script", callerNode.getVariableRef()); } else { log.trace("Analyze {} script: {}", callerNode.getVariableRef(), script); for(VariableRefCall variableRefCall : parseScript(script)) { VariableRefNode calleeNode = asNode(variableRefCall, callerNode.getValueTable()); callerNode.addCallee(calleeNode); getVariableRefNode(calleeNode); } } } @VisibleForTesting static Set<VariableRefCall> parseScript(String script) { String clearScript = clearScriptComments(script); ImmutableSet.Builder<VariableRefCall> builder = ImmutableSet.builder(); parseSingleArgGlobalMethod(clearScript, $_CALL, "$", builder); parseSingleArgGlobalMethod(clearScript, $THIS_CALL, "$this", builder); parseSingleArgGlobalMethod(clearScript, $VAR_CALL, "$var", builder); return builder.build(); } private static String clearScriptComments(String script) { AstRoot node = new Parser(COMPILER_ENVIRONS).parse(script, "script", 1); return node.toSource(); } private static void parseSingleArgGlobalMethod(CharSequence script, Pattern pattern, String method, ImmutableSet.Builder<VariableRefCall> builder) { Matcher matcher = pattern.matcher(script); while(matcher.find()) { if(matcher.groupCount() == 2) { builder.add(new VariableRefCall(method, matcher.group(1))); } } } private static VariableRefNode asNode(VariableRefCall variableRefCall, @NotNull ValueTable table) { MagmaEngineVariableResolver reference = MagmaEngineVariableResolver.valueOf(variableRefCall.getVariableRef()); switch(variableRefCall.getMethod()) { case "$": if(reference.getDatasourceName() == null || reference.getTableName() == null) { if(table.isView()) { ValueTable wrappedTable = ((ValueTableWrapper) table).getWrappedValueTable(); Variable variable = reference.resolveSource(wrappedTable).getVariable(); return new VariableRefNode(Variable.Reference.getReference(wrappedTable, variable), wrappedTable, getScript(variable)); } Variable variable = reference.resolveSource(table).getVariable(); return new VariableRefNode(Variable.Reference.getReference(table, variable), table, getScript(variable)); } Variable variable = reference.resolveSource().getVariable(); return new VariableRefNode(Variable.Reference .getReference(reference.getDatasourceName(), reference.getTableName(), variable.getName()), MagmaEngine.get().getDatasource(reference.getDatasourceName()).getValueTable(reference.getTableName()), getScript(variable)); case "$this": case "$var": Variable thisVariable = reference.resolveSource(table).getVariable(); return new VariableRefNode(Variable.Reference.getReference(table, thisVariable), table, getScript(thisVariable)); default: throw new MagmaJsEvaluationRuntimeException("Unsupported method validation for " + variableRefCall.getMethod()); } } @Nullable private static String getScript(AttributeAware variable) { return variable.hasAttribute(SCRIPT_ATTRIBUTE_NAME) // ? variable.getAttributeStringValue(SCRIPT_ATTRIBUTE_NAME) // : null; } @VisibleForTesting static class VariableRefCall { @NotNull private final String method; @NotNull private final String variableRef; VariableRefCall(@NotNull String method, @NotNull String variableRef) { this.method = method; this.variableRef = variableRef; } @NotNull public String getMethod() { return method; } @NotNull public String getVariableRef() { return variableRef; } @Override public String toString() { return method + "('" + variableRef + "')"; } @Override public int hashCode() { return Objects.hashCode(method, variableRef); } @Override public boolean equals(Object obj) { if(this == obj) return true; if(obj == null || getClass() != obj.getClass()) return false; VariableRefCall other = (VariableRefCall) obj; return Objects.equal(method, other.method) && Objects.equal(variableRef, other.variableRef); } } static class VariableRefNode implements Serializable { private static final long serialVersionUID = -6622597054116817497L; @NotNull private final String variableRef; @NotNull private transient final ValueTable valueTable; @Nullable private final String script; private final Set<VariableRefNode> callers = new HashSet<>(); private final Set<VariableRefNode> callees = new HashSet<>(); VariableRefNode(@NotNull String variableRef, @NotNull ValueTable valueTable, @Nullable String script) { this.variableRef = variableRef; this.valueTable = valueTable; this.script = script; } @NotNull public String getVariableRef() { return variableRef; } public Set<VariableRefNode> getCallers() { return callers; } public Set<VariableRefNode> getCallees() { return callees; } @Nullable public String getScript() { return script; } @NotNull public ValueTable getValueTable() { return valueTable; } public void addCallee(@NotNull VariableRefNode callee) throws CircularVariableDependencyException { callee.callers.add(this); callees.add(callee); checkCircularDependencies(callee, new HashSet<VariableRefNode>()); } private static void checkCircularDependencies(@Nullable VariableRefNode node, Collection<VariableRefNode> callersList) throws CircularVariableDependencyException { if(node == null) return; if(callersList.contains(node)) { throw new CircularVariableDependencyException(node); } callersList.add(node); for(VariableRefNode caller : node.getCallers()) { checkCircularDependencies(caller, callersList); } } @Override @SuppressWarnings("SimplifiableIfStatement") public boolean equals(Object o) { if(this == o) return true; if(!(o instanceof VariableRefNode)) return false; return variableRef.equals(((VariableRefNode) o).variableRef); } @Override public int hashCode() { return variableRef.hashCode(); } @Override public String toString() { return Objects.toStringHelper(this).omitNullValues().addValue(variableRef).add("callers", callers).toString(); } } }