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}