001/* 002 * ModeShape (http://www.modeshape.org) 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.modeshape.schematic.internal.schema; 017 018import java.security.AccessController; 019import java.security.PrivilegedAction; 020import java.util.ArrayList; 021import java.util.HashMap; 022import java.util.LinkedList; 023import java.util.List; 024import java.util.Map; 025import java.util.Properties; 026import java.util.StringTokenizer; 027import org.modeshape.schematic.SchemaLibrary; 028import org.modeshape.schematic.document.Document; 029import org.modeshape.schematic.document.Path; 030 031/** 032 * 033 */ 034public class DocumentTransformer { 035 036 private static final String CURLY_PREFIX = "${"; 037 private static final String CURLY_SUFFIX = "}"; 038 private static final String VAR_DELIM = ","; 039 private static final String DEFAULT_DELIM = ":"; 040 041 protected static interface PropertyAccessor { 042 String getProperty( String name ); 043 } 044 045 protected static final class PropertiesAccessor implements PropertyAccessor { 046 private final Properties properties; 047 048 protected PropertiesAccessor( Properties properties ) { 049 this.properties = properties; 050 } 051 052 @Override 053 public String getProperty( String name ) { 054 return properties.getProperty(name); 055 } 056 } 057 058 protected static final class SystemPropertyAccessor implements PropertyAccessor { 059 public static final SystemPropertyAccessor INSTANCE = new SystemPropertyAccessor(); 060 061 private SystemPropertyAccessor() { 062 // prevent instantiation 063 } 064 065 @Override 066 public String getProperty( final String name ) { 067 return AccessController.doPrivileged((PrivilegedAction<String>) () -> System.getProperty(name)); 068 } 069 } 070 071 /** 072 * getSubstitutedProperty is called to perform the property substitution on the value. 073 * 074 * @param value 075 * @param propertyAccessor 076 * @return String 077 */ 078 public static String getSubstitutedProperty( String value, 079 PropertyAccessor propertyAccessor ) { 080 081 if (value == null || value.trim().length() == 0) return value; 082 083 StringBuilder sb = new StringBuilder(value); 084 085 // Get the index of the first constant, if any 086 int startName = sb.indexOf(CURLY_PREFIX); 087 088 if (startName == -1) return value; 089 090 // process as many different variable groupings that are defined, where one group will resolve to one property 091 // substitution 092 while (startName != -1) { 093 String defaultValue = null; 094 095 int endName = sb.indexOf(CURLY_SUFFIX, startName); 096 097 if (endName == -1) { 098 // if no suffix can be found, then this variable was probably defined incorrectly 099 // but return what there is at this point 100 return sb.toString(); 101 } 102 103 String varString = sb.substring(startName + 2, endName); 104 if (varString.indexOf(DEFAULT_DELIM) > -1) { 105 List<String> defaults = split(varString, DEFAULT_DELIM); 106 107 // get the property(s) variables that are defined left of the default delimiter. 108 varString = defaults.get(0); 109 110 // if the default is defined, then capture in case none of the other properties are found 111 if (defaults.size() == 2) { 112 defaultValue = defaults.get(1); 113 } 114 } 115 116 String constValue = null; 117 // split the property(s) based VAR_DELIM, when multiple property options are defined 118 List<String> vars = split(varString, VAR_DELIM); 119 for (final String var : vars) { 120 constValue = System.getenv(var); 121 if (constValue == null) { 122 constValue = propertyAccessor.getProperty(var); 123 } 124 125 // the first found property is the value to be substituted 126 if (constValue != null) { 127 break; 128 } 129 } 130 131 // if no property is found to substitute, then use the default value, if defined 132 if (constValue == null && defaultValue != null) { 133 constValue = defaultValue; 134 } 135 136 if (constValue != null) { 137 sb = sb.replace(startName, endName + 1, constValue); 138 // Checking for another constants 139 startName = sb.indexOf(CURLY_PREFIX); 140 141 } else { 142 // continue to try to substitute for other properties so that all defined variables 143 // are tried to be substituted for 144 startName = sb.indexOf(CURLY_PREFIX, endName); 145 146 } 147 148 } 149 150 return sb.toString(); 151 } 152 153 /** 154 * Split a string into pieces based on delimiters. Similar to the perl function of the same name. The delimiters are not 155 * included in the returned strings. 156 * 157 * @param str Full string 158 * @param splitter Characters to split on 159 * @return List of String pieces from full string 160 */ 161 private static List<String> split( String str, 162 String splitter ) { 163 StringTokenizer tokens = new StringTokenizer(str, splitter); 164 ArrayList<String> l = new ArrayList<>(tokens.countTokens()); 165 while (tokens.hasMoreTokens()) { 166 l.add(tokens.nextToken()); 167 } 168 return l; 169 } 170 171 /** 172 * An implementation of {@link Document.ValueTransformer} that replaces variables in the 173 * field values with values from the system properties. Only string values are considered, since other types cannot contain 174 * variables (and since the transformers are never called on Document or List values). 175 * <p> 176 * Variables may appear anywhere within a string value, and multiple variables can be used within the same value. Variables 177 * take the form: 178 * 179 * <pre> 180 * variable := '${' variableNames [ ':' defaultValue ] '}' 181 * 182 * variableNames := variableName [ ',' variableNames ] 183 * 184 * variableName := /* any characters except ',' and ':' and '}' 185 * 186 * defaultValue := /* any characters except 187 * </pre> 188 * 189 * Note that <i>variableName</i> is the name used to look up a the property. 190 * </p> 191 * Notice that the syntax supports multiple <i>variables</i>. The logic will process the <i>variables</i> from let to right, 192 * until an existing property is found. And at that point, it will stop and will not attempt to find values for the other 193 * <i>variables</i>. 194 * <p> 195 */ 196 public static final class PropertiesTransformer implements Document.ValueTransformer { 197 198 private final PropertiesAccessor accessor; 199 200 public PropertiesTransformer( Properties properties ) { 201 this.accessor = new PropertiesAccessor(properties); 202 } 203 204 @Override 205 public Object transform( String name, 206 Object value ) { 207 // Only look at string values ... 208 if (value instanceof String) { 209 return getSubstitutedProperty((String)value, this.accessor); 210 } 211 return value; 212 } 213 } 214 215 /** 216 * An implementation of {@link Document.ValueTransformer} that replaces variables in the 217 * field values with values from the system properties. Only string values are considered, since other types cannot contain 218 * variables (and since the transformers are never called on Document or List values). 219 * <p> 220 * Variables may appear anywhere within a string value, and multiple variables can be used within the same value. Variables 221 * take the form: 222 * 223 * <pre> 224 * variable := '${' variableNames [ ':' defaultValue ] '}' 225 * 226 * variableNames := variableName [ ',' variableNames ] 227 * 228 * variableName := /* any characters except ',' and ':' and '}' 229 * 230 * defaultValue := /* any characters except 231 * </pre> 232 * 233 * Note that <i>variableName</i> is the name used to look up a System property via {@link System#getProperty(String)}. 234 * </p> 235 * Notice that the syntax supports multiple <i>variables</i>. The logic will process the <i>variables</i> from let to right, 236 * until an existing System property is found. And at that point, it will stop and will not attempt to find values for the 237 * other <i>variables</i>. 238 * <p> 239 */ 240 public static final class SystemPropertiesTransformer implements Document.ValueTransformer { 241 242 @Override 243 public Object transform( String name, 244 Object value ) { 245 // Only look at string values ... 246 if (value instanceof String) { 247 return getSubstitutedProperty((String)value, SystemPropertyAccessor.INSTANCE); 248 } 249 return value; 250 } 251 } 252 253 /** 254 * Return a copy of the supplied document that contains converted values for all of the fields (including in the nested 255 * documents and arrays) that have values that are of the wrong type but can be converted to be of the correct type. 256 * <p> 257 * This method does nothing and returns the original document if there are no changes to be made. 258 * </p> 259 * 260 * @param original the original document that contains fields with mismatched values; may not be null 261 * @param results the results of the {@link SchemaLibrary#validate(Document, String) JSON Schema 262 * validation} and which contains the {@link SchemaLibrary.MismatchedTypeProblem type mismatch errors} 263 * @return the document with all of the conversions made the its fields and the fields of nested documents, or the original 264 * document if there are no conversions to be made; never null 265 */ 266 public static Document convertValuesWithMismatchedTypes( Document original, 267 SchemaLibrary.Results results ) { 268 if (results == null || !results.hasProblems()) return original; 269 270 // Create a conversion object for each of the field values with mismatched (but convertable) types ... 271 LinkedList<Conversion> conversions = new LinkedList<>(); 272 for (SchemaLibrary.Problem problem : results) { 273 if (problem instanceof SchemaLibrary.MismatchedTypeProblem) { 274 conversions.add(new Conversion((SchemaLibrary.MismatchedTypeProblem)problem)); 275 } 276 } 277 if (conversions.isEmpty()) return original; 278 279 // Transform the original document, starting at the first level ... 280 return convertValuesWithMismatchedTypes(original, 0, conversions); 281 } 282 283 protected static Document convertValuesWithMismatchedTypes( Document original, 284 int level, 285 LinkedList<Conversion> conversions ) { 286 // Create a placeholder for the new field values for this document ... 287 Map<String, Object> changedFields = new HashMap<>(); 288 289 // Now apply the changes to this document and prepare to coallesce the changes for the nested documents ... 290 int nextLevel = level + 1; 291 Map<String, LinkedList<Conversion>> nextLevelConversionsBySegment = new HashMap<>(); 292 for (Conversion conversion : conversions) { 293 Path path = conversion.getPath(); 294 assert path.size() > level; 295 String segment = path.get(level); 296 if (path.size() == nextLevel) { 297 // This is the last segment for this path, so change the output document's field ... 298 changedFields.put(segment, conversion.getConvertedValue()); 299 } else { 300 // Otherwise, the path is for the nested document ... 301 LinkedList<Conversion> nestedConversions = nextLevelConversionsBySegment.get(segment); 302 if (nestedConversions == null) { 303 nestedConversions = new LinkedList<>(); 304 nextLevelConversionsBySegment.put(segment, nestedConversions); 305 } 306 nestedConversions.add(conversion); 307 } 308 } 309 310 // Now apply all of the conversions for the nested documents, 311 // getting the results and storing them in the 'changedFields' ... 312 for (Map.Entry<String, LinkedList<Conversion>> entry : nextLevelConversionsBySegment.entrySet()) { 313 String segment = entry.getKey(); 314 LinkedList<Conversion> nestedConversions = entry.getValue(); 315 Document nested = original.getDocument(segment); 316 Document newDoc = convertValuesWithMismatchedTypes(nested, nextLevel, nestedConversions); 317 changedFields.put(segment, newDoc); 318 } 319 320 // Now create a copy of the original document but with the changed fields ... 321 return original.with(changedFields); 322 } 323 324 protected static final class Conversion implements Comparable<Conversion> { 325 private final SchemaLibrary.MismatchedTypeProblem problem; 326 327 protected Conversion( SchemaLibrary.MismatchedTypeProblem problem ) { 328 this.problem = problem; 329 } 330 331 @Override 332 public int compareTo( Conversion that ) { 333 if (this == that) return 0; 334 return this.problem.getPath().compareTo(that.problem.getPath()); 335 } 336 337 public Path getPath() { 338 return this.problem.getPath(); 339 } 340 341 public Object getConvertedValue() { 342 return this.problem.getConvertedValue(); 343 } 344 } 345 346}