org.modeshape.schematic.internal.schema.DocumentTransformer Maven / Gradle / Ivy
Show all versions of modeshape-schematic Show documentation
/*
* ModeShape (http://www.modeshape.org)
*
* 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.modeshape.schematic.internal.schema;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import org.modeshape.schematic.SchemaLibrary;
import org.modeshape.schematic.document.Document;
import org.modeshape.schematic.document.Path;
/**
*
*/
public class DocumentTransformer {
private static final String CURLY_PREFIX = "${";
private static final String CURLY_SUFFIX = "}";
private static final String VAR_DELIM = ",";
private static final String DEFAULT_DELIM = ":";
protected static interface PropertyAccessor {
String getProperty( String name );
}
protected static final class PropertiesAccessor implements PropertyAccessor {
private final Properties properties;
protected PropertiesAccessor( Properties properties ) {
this.properties = properties;
}
@Override
public String getProperty( String name ) {
return properties.getProperty(name);
}
}
protected static final class SystemPropertyAccessor implements PropertyAccessor {
public static final SystemPropertyAccessor INSTANCE = new SystemPropertyAccessor();
private SystemPropertyAccessor() {
// prevent instantiation
}
@Override
public String getProperty( final String name ) {
return AccessController.doPrivileged((PrivilegedAction) () -> System.getProperty(name));
}
}
/**
* getSubstitutedProperty is called to perform the property substitution on the value.
*
* @param value
* @param propertyAccessor
* @return String
*/
public static String getSubstitutedProperty( String value,
PropertyAccessor propertyAccessor ) {
if (value == null || value.trim().length() == 0) return value;
StringBuilder sb = new StringBuilder(value);
// Get the index of the first constant, if any
int startName = sb.indexOf(CURLY_PREFIX);
if (startName == -1) return value;
// process as many different variable groupings that are defined, where one group will resolve to one property
// substitution
while (startName != -1) {
String defaultValue = null;
int endName = sb.indexOf(CURLY_SUFFIX, startName);
if (endName == -1) {
// if no suffix can be found, then this variable was probably defined incorrectly
// but return what there is at this point
return sb.toString();
}
String varString = sb.substring(startName + 2, endName);
if (varString.indexOf(DEFAULT_DELIM) > -1) {
List defaults = split(varString, DEFAULT_DELIM);
// get the property(s) variables that are defined left of the default delimiter.
varString = defaults.get(0);
// if the default is defined, then capture in case none of the other properties are found
if (defaults.size() == 2) {
defaultValue = defaults.get(1);
}
}
String constValue = null;
// split the property(s) based VAR_DELIM, when multiple property options are defined
List vars = split(varString, VAR_DELIM);
for (final String var : vars) {
constValue = System.getenv(var);
if (constValue == null) {
constValue = propertyAccessor.getProperty(var);
}
// the first found property is the value to be substituted
if (constValue != null) {
break;
}
}
// if no property is found to substitute, then use the default value, if defined
if (constValue == null && defaultValue != null) {
constValue = defaultValue;
}
if (constValue != null) {
sb = sb.replace(startName, endName + 1, constValue);
// Checking for another constants
startName = sb.indexOf(CURLY_PREFIX);
} else {
// continue to try to substitute for other properties so that all defined variables
// are tried to be substituted for
startName = sb.indexOf(CURLY_PREFIX, endName);
}
}
return sb.toString();
}
/**
* Split a string into pieces based on delimiters. Similar to the perl function of the same name. The delimiters are not
* included in the returned strings.
*
* @param str Full string
* @param splitter Characters to split on
* @return List of String pieces from full string
*/
private static List split( String str,
String splitter ) {
StringTokenizer tokens = new StringTokenizer(str, splitter);
ArrayList l = new ArrayList<>(tokens.countTokens());
while (tokens.hasMoreTokens()) {
l.add(tokens.nextToken());
}
return l;
}
/**
* An implementation of {@link Document.ValueTransformer} that replaces variables in the
* field values with values from the system properties. Only string values are considered, since other types cannot contain
* variables (and since the transformers are never called on Document or List values).
*
* Variables may appear anywhere within a string value, and multiple variables can be used within the same value. Variables
* take the form:
*
*
* variable := '${' variableNames [ ':' defaultValue ] '}'
*
* variableNames := variableName [ ',' variableNames ]
*
* variableName := /* any characters except ',' and ':' and '}'
*
* defaultValue := /* any characters except
*
*
* Note that variableName is the name used to look up a the property.
*
* Notice that the syntax supports multiple variables. The logic will process the variables from let to right,
* until an existing property is found. And at that point, it will stop and will not attempt to find values for the other
* variables.
*
*/
public static final class PropertiesTransformer implements Document.ValueTransformer {
private final PropertiesAccessor accessor;
public PropertiesTransformer( Properties properties ) {
this.accessor = new PropertiesAccessor(properties);
}
@Override
public Object transform( String name,
Object value ) {
// Only look at string values ...
if (value instanceof String) {
return getSubstitutedProperty((String)value, this.accessor);
}
return value;
}
}
/**
* An implementation of {@link Document.ValueTransformer} that replaces variables in the
* field values with values from the system properties. Only string values are considered, since other types cannot contain
* variables (and since the transformers are never called on Document or List values).
*
* Variables may appear anywhere within a string value, and multiple variables can be used within the same value. Variables
* take the form:
*
*
* variable := '${' variableNames [ ':' defaultValue ] '}'
*
* variableNames := variableName [ ',' variableNames ]
*
* variableName := /* any characters except ',' and ':' and '}'
*
* defaultValue := /* any characters except
*
*
* Note that variableName is the name used to look up a System property via {@link System#getProperty(String)}.
*
* Notice that the syntax supports multiple variables. The logic will process the variables from let to right,
* until an existing System property is found. And at that point, it will stop and will not attempt to find values for the
* other variables.
*
*/
public static final class SystemPropertiesTransformer implements Document.ValueTransformer {
@Override
public Object transform( String name,
Object value ) {
// Only look at string values ...
if (value instanceof String) {
return getSubstitutedProperty((String)value, SystemPropertyAccessor.INSTANCE);
}
return value;
}
}
/**
* Return a copy of the supplied document that contains converted values for all of the fields (including in the nested
* documents and arrays) that have values that are of the wrong type but can be converted to be of the correct type.
*
* This method does nothing and returns the original document if there are no changes to be made.
*
*
* @param original the original document that contains fields with mismatched values; may not be null
* @param results the results of the {@link SchemaLibrary#validate(Document, String) JSON Schema
* validation} and which contains the {@link SchemaLibrary.MismatchedTypeProblem type mismatch errors}
* @return the document with all of the conversions made the its fields and the fields of nested documents, or the original
* document if there are no conversions to be made; never null
*/
public static Document convertValuesWithMismatchedTypes( Document original,
SchemaLibrary.Results results ) {
if (results == null || !results.hasProblems()) return original;
// Create a conversion object for each of the field values with mismatched (but convertable) types ...
LinkedList conversions = new LinkedList<>();
for (SchemaLibrary.Problem problem : results) {
if (problem instanceof SchemaLibrary.MismatchedTypeProblem) {
conversions.add(new Conversion((SchemaLibrary.MismatchedTypeProblem)problem));
}
}
if (conversions.isEmpty()) return original;
// Transform the original document, starting at the first level ...
return convertValuesWithMismatchedTypes(original, 0, conversions);
}
protected static Document convertValuesWithMismatchedTypes( Document original,
int level,
LinkedList conversions ) {
// Create a placeholder for the new field values for this document ...
Map changedFields = new HashMap<>();
// Now apply the changes to this document and prepare to coallesce the changes for the nested documents ...
int nextLevel = level + 1;
Map> nextLevelConversionsBySegment = new HashMap<>();
for (Conversion conversion : conversions) {
Path path = conversion.getPath();
assert path.size() > level;
String segment = path.get(level);
if (path.size() == nextLevel) {
// This is the last segment for this path, so change the output document's field ...
changedFields.put(segment, conversion.getConvertedValue());
} else {
// Otherwise, the path is for the nested document ...
LinkedList nestedConversions = nextLevelConversionsBySegment.get(segment);
if (nestedConversions == null) {
nestedConversions = new LinkedList<>();
nextLevelConversionsBySegment.put(segment, nestedConversions);
}
nestedConversions.add(conversion);
}
}
// Now apply all of the conversions for the nested documents,
// getting the results and storing them in the 'changedFields' ...
for (Map.Entry> entry : nextLevelConversionsBySegment.entrySet()) {
String segment = entry.getKey();
LinkedList nestedConversions = entry.getValue();
Document nested = original.getDocument(segment);
Document newDoc = convertValuesWithMismatchedTypes(nested, nextLevel, nestedConversions);
changedFields.put(segment, newDoc);
}
// Now create a copy of the original document but with the changed fields ...
return original.with(changedFields);
}
protected static final class Conversion implements Comparable {
private final SchemaLibrary.MismatchedTypeProblem problem;
protected Conversion( SchemaLibrary.MismatchedTypeProblem problem ) {
this.problem = problem;
}
@Override
public int compareTo( Conversion that ) {
if (this == that) return 0;
return this.problem.getPath().compareTo(that.problem.getPath());
}
public Path getPath() {
return this.problem.getPath();
}
public Object getConvertedValue() {
return this.problem.getConvertedValue();
}
}
}