/*
* Copyright 2015 Lukas Krejci
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package org.revapi.configuration;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.ref.WeakReference;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nonnull;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;
import org.jboss.dmr.ModelNode;
/**
* @see #validate(org.jboss.dmr.ModelNode, Configurable)
*
* @author Lukas Krejci
* @since 0.1
*/
public final class ConfigurationValidator {
private static class PartialValidationResult {
final String rootPath;
final ModelNode results;
private PartialValidationResult(String rootPath, ModelNode results) {
this.rootPath = rootPath;
this.results = results;
}
}
private WeakReference<ScriptEngine> jsEngine;
/**
* Validates that the full configuration contains valid configuration for given configurable.
*
* @param fullConfiguration the full configuration containing properties for all configurables
* @param configurable the configurable to validate the configuration for
*
* @return the result of the validation.
*
* @throws ConfigurationException if reading the JSON schemas of the configurable failed
*/
public ValidationResult validate(@Nonnull ModelNode fullConfiguration, @Nonnull Configurable configurable)
throws ConfigurationException {
try {
String[] rootPaths = configurable.getConfigurationRootPaths();
if (rootPaths == null || rootPaths.length == 0) {
return ValidationResult.success();
}
StringWriter output = new StringWriter();
ScriptEngine js = getJsEngine(output);
List<PartialValidationResult> validationResults = new ArrayList<>();
for (String rootPath : rootPaths) {
String[] path = rootPath.split("\\.");
ModelNode configNode = fullConfiguration.get(path);
if (!configNode.isDefined()) {
continue;
}
try (Reader schemaReader = configurable.getJSONSchema(rootPath)) {
if (schemaReader == null) {
continue;
}
String schema = read(schemaReader);
StringWriter configJSONWrt = new StringWriter();
PrintWriter wrt = new PrintWriter(configJSONWrt);
configNode.writeJSONString(wrt, true);
String config = configJSONWrt.toString();
Bindings variables = js.createBindings();
js.eval("var data = " + config + ";", variables);
try {
js.eval("var schema = " + schema + ";", variables);
} catch (ScriptException e) {
throw new IllegalArgumentException("Failed to parse the schema: " + schema, e);
}
variables.put("tv4", js.getContext().getAttribute("tv4", ScriptContext.GLOBAL_SCOPE));
Object resultObject = js.eval("tv4.validateMultiple(data, schema)", variables);
ModelNode result = JSONUtil.toModelNode(resultObject);
PartialValidationResult r = new PartialValidationResult(rootPath, result);
validationResults.add(r);
}
}
return convert(validationResults);
} catch (IOException | ScriptException e) {
throw new ConfigurationException("Failed to validate configuration.", e);
}
}
private ValidationResult convert(List<PartialValidationResult> results) {
ModelNode result = new ModelNode();
for (PartialValidationResult r : results) {
if (r.results.has("errors")) {
List<ModelNode> errors = r.results.get("errors").asList();
for (ModelNode error : errors) {
if (error.has("dataPath")) {
error.get("dataPath")
.set("/" + r.rootPath.replace(".", "/") + error.get("dataPath").asString());
}
}
}
boolean valid =
r.results.get("valid").asBoolean() && ((!result.has("valid")) || result.get("valid").asBoolean());
result.get("valid").set(valid);
if (result.has("errors")) {
for (ModelNode e : r.results.get("errors").asList()) {
result.get("errors").add(e);
}
} else {
result.get("errors").set(r.results.get("errors"));
}
if (result.has("missing")) {
for (ModelNode m : r.results.get("missing").asList()) {
result.get("missing").add(m.asString());
}
} else {
result.get("missing").set(r.results.get("missing"));
}
}
return result.isDefined() ? ValidationResult.fromTv4Results(result) : new ValidationResult(null, null);
}
private ScriptEngine getJsEngine(Writer output) throws IOException, ScriptException {
ScriptEngine ret = null;
if (jsEngine != null) {
ret = jsEngine.get();
}
if (ret == null) {
ret = new ScriptEngineManager().getEngineByName("javascript");
ScriptContext ctx = new SimpleScriptContext();
Bindings globalScope = ret.createBindings();
ctx.setBindings(globalScope, ScriptContext.GLOBAL_SCOPE);
initTv4(ret, globalScope);
ret.setContext(ctx);
jsEngine = new WeakReference<>(ret);
}
ret.getContext().setWriter(output);
ret.getContext().setErrorWriter(output);
return ret;
}
private void initTv4(ScriptEngine engine, Bindings bindings) throws IOException, ScriptException {
try (Reader rdr = new InputStreamReader(getClass().getResourceAsStream("/tv4.min.js"),
Charset.forName("UTF-8"))) {
engine.eval(rdr, bindings);
}
}
private static String read(Reader rdr) throws IOException {
StringBuilder bld = new StringBuilder();
char[] buffer = new char[4096];
int cnt;
while ((cnt = rdr.read(buffer)) != -1) {
bld.append(buffer, 0, cnt);
}
return bld.toString();
}
}