View Javadoc
1   /*
2    * Copyright 2010 The Kuali Foundation.
3    * 
4    * Licensed under the Educational Community License, Version 1.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    * http://www.opensource.org/licenses/ecl1.php
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.ole.sys.context;
17  
18  import static org.kuali.ole.sys.OLEConstants.SchemaBuilder.DD_VALIDATION_PREFIX;
19  import static org.kuali.ole.sys.OLEConstants.SchemaBuilder.SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_BEGIN;
20  import static org.kuali.ole.sys.OLEConstants.SchemaBuilder.SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_END;
21  import static org.kuali.ole.sys.OLEConstants.SchemaBuilder.XSD_VALIDATION_PREFIX;
22  
23  import java.io.File;
24  import java.io.IOException;
25  import java.util.ArrayList;
26  import java.util.Collection;
27  import java.util.HashSet;
28  import java.util.Iterator;
29  import java.util.Set;
30  
31  import org.apache.commons.io.FileUtils;
32  import org.apache.commons.lang.StringUtils;
33  import org.apache.log4j.BasicConfigurator;
34  import org.apache.log4j.Level;
35  import org.apache.log4j.Logger;
36  
37  /**
38   * Called during the build process to output schema files with validation built from the data dictionary or set to defaults
39   */
40  public class SchemaBuilder {
41      private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(SchemaBuilder.class);
42      private static Level logLevel = Level.INFO;
43  
44      /**
45       * <pre>
46       * Performs schema build process. 
47       * 
48       * Build directory path containing the schema files, static directory that schema files will be
49       * outputted to, and flag for whether to use data dictionary validation all must given as arguments. 
50       * 
51       * Schema files in build directory should contain place-holders for which the validation will be substituted. The place-holder begin symbol is ${,
52       * and the end symbol is }. Then the place-holder should contain two parts, first the xsd type to use if data dictionary
53       * validation is not on. The second is the data dictionary entry (businessObjectEntry.attributeName) prefixed with 'dd:' that
54       * will be pulled for dd validation. The parts should be separated with a comma. Any type values without a place-holder will
55       * not be modified. 
56       * 
57       * Program also fills in externalizable.static.content.url place-holder. Value to set should be passed as the fourth program argument
58       * </pre>
59       * 
60       * @param args
61       */
62      public static void main(String[] args) {
63          if (args.length < 5) {
64              System.err.println("ERROR: You must pass the build directory, static directory, dd flag, external content url, and rebuild types flag as arguments");
65              System.exit(8);
66          }
67          try {
68              // initialize log4j
69              BasicConfigurator.configure();
70              Logger.getRootLogger().setLevel(Level.WARN);
71              LOG.setLevel(logLevel);
72  
73              String buildDirectoryPath = args[0];
74              if (StringUtils.isBlank(buildDirectoryPath)) {
75                  logAndThrowException("Build directory must be passed as first argument");
76              }
77              if (LOG.isDebugEnabled()) {
78                  LOG.debug("Build directory set to " + buildDirectoryPath);
79              }
80  
81              String staticDirectoryPath = args[1];
82              if (StringUtils.isBlank(staticDirectoryPath)) {
83                  logAndThrowException("Static directory must be passed as second argument");
84              }
85              if (LOG.isDebugEnabled()) {
86                  LOG.debug("Static directory set to " + staticDirectoryPath);
87              }
88  
89              String dataDictionaryValidation = args[2];
90              if (StringUtils.isBlank(dataDictionaryValidation)) {
91                  logAndThrowException("Use data dictionary validation must be passed as third argument");
92              }
93  
94              String externalizableContentUrl = args[3];
95              if (StringUtils.isBlank(externalizableContentUrl)) {
96                  logAndThrowException("Externalizalbe static content URL must be passed as fourth argument");
97              }
98  
99              String rebuildDDTypesFlag = args[4];
100             if (StringUtils.isBlank(rebuildDDTypesFlag)) {
101                 logAndThrowException("Rebuild DD flags must be passed as fifth argument");
102             }
103 
104             boolean useDataDictionaryValidation = Boolean.parseBoolean(dataDictionaryValidation);
105             boolean rebuildDDTypes = Boolean.parseBoolean(rebuildDDTypesFlag);
106             if (LOG.isDebugEnabled()) {
107                 LOG.debug("Use data dictionary validation set to " + useDataDictionaryValidation);
108             }
109 
110             // if using dd validation must start up spring so we can read DD
111             if (useDataDictionaryValidation && rebuildDDTypes) {
112                 SpringContextForBatchRunner.initializeKfs();
113             }
114 
115             LOG.debug("Getting build schema files");
116             Collection buildSchemaFiles = getBuildSchemaFiles(buildDirectoryPath);
117 
118             LOG.debug("Building schema files");
119             try {
120                 buildSchemaFiles(buildSchemaFiles, staticDirectoryPath, buildDirectoryPath, useDataDictionaryValidation, externalizableContentUrl, rebuildDDTypes);
121             }
122             catch (IOException ex) {
123                 LOG.error("Error building schema files: " + ex.getMessage(), ex);
124                 throw new RuntimeException("Error building schema files: " + ex.getMessage(), ex);
125             }
126 
127             LOG.info("Finished building schema files.");
128             System.exit(0);
129         }
130         catch (Throwable t) {
131             System.err.println("ERROR: Exception caught: " + t.getMessage());
132             t.printStackTrace(System.err);
133             System.exit(8);
134         }
135     }
136 
137     /**
138      * Returns Collection of File objects for all .xsd files found in given directory (including sub-directories)
139      * 
140      * @param buildDirectoryPath Directory to look for schema files
141      * @return Collection of File objects
142      */
143     protected static Collection getBuildSchemaFiles(String buildDirectoryPath) {
144         File buildDirectory = new File(buildDirectoryPath);
145 
146         return FileUtils.listFiles(buildDirectory, new String[] { "xsd" }, true);
147     }
148 
149     /**
150      * Iterates through build schema files processing validation place-holders and outputting to static directory. Include file
151      * for referenced schema types is also written out
152      * 
153      * @param buildSchemaFiles collection of File objects for build schema files
154      * @param staticDirectoryPath path that processed schema files will be written to
155      * @param buildDirectoryPath path of build schema files
156      * @param useDataDictionaryValidation indicates whether data dictionary validation should be used, if false the general xsd
157      *            datatype in the place-holder will be used
158      * @param externalizableContentUrl URL to set for externalizable.static.content.url token
159      * @throws IOException thrown for any read/write errors encountered
160      */
161     protected static void buildSchemaFiles(Collection buildSchemaFiles, String staticDirectoryPath, String buildDirectoryPath, boolean useDataDictionaryValidation, String externalizableContentUrl, boolean rebuildDDTypes) throws IOException {
162         // initialize dd type schema
163         Collection typesSchemaLines = initalizeDataDictionaryTypesSchema();
164         Set<String> builtTypes = new HashSet<String>();
165 
166         // convert static directory path to abstract path
167         File staticDirectory = new File(staticDirectoryPath);
168         String staticPathName = staticDirectory.getAbsolutePath();
169 
170         for (Iterator iterator = buildSchemaFiles.iterator(); iterator.hasNext();) {
171             File buildSchemFile = (File) iterator.next();
172             if (LOG.isDebugEnabled()) {
173                 LOG.debug("Processing schema file: " + buildSchemFile.getName());
174             }
175 
176             String outSchemaFilePathName = staticPathName + getRelativeFilePathName(buildSchemFile, buildDirectoryPath);
177             LOG.info("Building schema file: " + outSchemaFilePathName);
178 
179             buildSchemaFile(buildSchemFile, outSchemaFilePathName, useDataDictionaryValidation, typesSchemaLines, externalizableContentUrl, builtTypes, rebuildDDTypes);
180         }
181 
182         // finalize dd type schema
183         typesSchemaLines.addAll(finalizeDataDictionaryTypesSchema());
184 
185         if (rebuildDDTypes) {
186             LOG.debug("Writing ddTypes schema file");
187             File ddTypesFile = new File(staticPathName + File.separator + "xsd" + File.separator + "sys" + File.separator + "ddTypes.xsd");
188             File ddTypesFileBuild = new File(buildDirectoryPath + File.separator + "xsd" + File.separator + "sys" + File.separator + "ddTypes.xsd");
189             FileUtils.writeLines(ddTypesFile, typesSchemaLines);
190             FileUtils.copyFile(ddTypesFile, ddTypesFileBuild);
191         }
192     }
193 
194     /**
195      * Process a single schema file (setting validation and externalizable token) and outputs to static directory. Any new data
196      * dictionary types encountered are added to the given Collection for later writing to the types include file
197      * 
198      * @param buildSchemFile build schema file that should be processed
199      * @param outSchemaFilePathName full file path name for the outputted schema
200      * @param useDataDictionaryValidation indicates whether data dictionary validation should be used, if false the general xsd
201      *            datatype in the place-holder will be used
202      * @param typesSchemaLines collection of type XML lines to add to for any new types
203      * @param externalizableContentUrl URL to set for externalizable.static.content.url token
204      * @param builtTypes - Set of attribute names for which a schema validation type has been built
205      * @throws IOException thrown for any read/write errors encountered
206      */
207     protected static void buildSchemaFile(File buildSchemFile, String outSchemaFilePathName, boolean useDataDictionaryValidation, Collection typesSchemaLines, String externalizableContentUrl, Set<String> builtTypes, boolean rebuildDDTypes) throws IOException {
208         Collection buildSchemaLines = FileUtils.readLines(buildSchemFile);
209         Collection outSchemaLines = new ArrayList();
210         int lineCount = 1;
211         for (Iterator iterator = buildSchemaLines.iterator(); iterator.hasNext();) {
212             if (LOG.isDebugEnabled()) {
213                 LOG.debug("Processing line " + lineCount + "of file " + buildSchemFile.getAbsolutePath());
214             }
215             String buildLine = (String) iterator.next();
216             String outLine = buildLine;
217 
218             // check for externalizable.static.content.url token and replace if found
219             if (StringUtils.contains(buildLine, "@externalizable.static.content.url@")) {
220                 outLine = StringUtils.replace(buildLine, "@externalizable.static.content.url@", externalizableContentUrl);
221             }
222 
223             // check for validation place-holder and process if found
224             else if (StringUtils.contains(buildLine, SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_BEGIN) && StringUtils.contains(buildLine, SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_END)) {
225                 String validationPlaceholder = StringUtils.substringBetween(buildLine, SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_BEGIN, SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_END);
226                 if (StringUtils.isBlank(validationPlaceholder)) {
227                     logAndThrowException(String.format("File %s line %s: validation placeholder cannot be blank", buildSchemFile.getAbsolutePath(), lineCount));
228                 }
229 
230                 if (LOG.isDebugEnabled()) {
231                     LOG.debug("Found dd validation placeholder: " + validationPlaceholder);
232                 }
233                 if (!StringUtils.contains(validationPlaceholder, ",")) {
234                     logAndThrowException(String.format("File %s, line %s: Invalid format of placehoder value: %s, must contain a ',' seperating parts", buildSchemFile.getAbsolutePath(), lineCount, validationPlaceholder));
235                 }
236 
237                 outLine = processValidationPlaceholder(validationPlaceholder, buildLine, buildSchemFile.getAbsolutePath(), lineCount, useDataDictionaryValidation, typesSchemaLines, builtTypes, rebuildDDTypes);
238             }
239 
240             outSchemaLines.add(outLine);
241             lineCount++;
242         }
243 
244         LOG.debug("Writing schema file to static directory");
245         File schemaFile = new File(outSchemaFilePathName);
246         FileUtils.writeLines(schemaFile, outSchemaLines);
247     }
248 
249     /**
250      * Performs logic to processes a validation place-holder for a line. First collects the configuration given in the
251      * place-holder (general xsd type and data dictionary attribute). If use data dictionary validation is set to false, then the
252      * place-holder will be set to the xsd type. If data dictionary validation is set to true, the general xsd type will be
253      * removed, and the corresponding the data dictionary will be consulted to build the dd type
254      * 
255      * <pre>
256      * ex. type="${xsd:token,dd:Chart.chartOfAccountsCode}" with useDataDictionaryValidation=false becomes type="xsd:token"
257      *    type="${xsd:token,dd:Chart.chartOfAccountsCode}" with useDataDictionaryValidation=true becomes type="dd:Chart.chartOfAccountsCode" and XML lines created for dd Types file
258      * </pre>
259      * 
260      * @param validationPlaceholder the parsed place-holder contents
261      * @param buildLine the complete line being read
262      * @param fileName the name for the file being processed
263      * @param lineCount count for the line being read
264      * @param useDataDictionaryValidation indicates whether data dictionary validation should be used, if false the general xsd
265      *            datatype in the place-holder will be used
266      * @param typesSchemaLines collection of type XML lines to add to for any new types
267      * @param builtTypes - Set of attribute names for which a schema validation type has been built
268      * @return String the out XML line (which validation filled in)
269      */
270     protected static String processValidationPlaceholder(String validationPlaceholder, String buildLine, String fileName, int lineCount, boolean useDataDictionaryValidation, Collection typesSchemaLines, Set<String> builtTypes, boolean rebuildDDTypes) {
271         String orignalPlaceholderValue = validationPlaceholder;
272 
273         // remove whitespace
274         validationPlaceholder = StringUtils.deleteWhitespace(validationPlaceholder);
275 
276         // get two parts of validation place-holder
277         String[] validationParts = StringUtils.split(validationPlaceholder, ",");
278         String xsdValidation = validationParts[0];
279         if (StringUtils.isBlank(xsdValidation) || !xsdValidation.startsWith(XSD_VALIDATION_PREFIX)) {
280             logAndThrowException(String.format("File %s, line %s: specified xsd validation is invalid, must start with %s", fileName, lineCount, XSD_VALIDATION_PREFIX));
281         }
282 
283         String ddAttributeName = validationParts[1];
284         if (StringUtils.isBlank(ddAttributeName) || !ddAttributeName.startsWith(DD_VALIDATION_PREFIX)) {
285             logAndThrowException(String.format("File %s, line %s: specified dd validation is invalid, must start with %s", fileName, lineCount, DD_VALIDATION_PREFIX));
286         }
287 
288         String outLine = buildLine;
289         if (useDataDictionaryValidation) {
290             if (LOG.isDebugEnabled()) {
291                 LOG.debug("Setting validation to use type: " + ddAttributeName);
292             }
293             outLine = StringUtils.replace(outLine, SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_BEGIN + orignalPlaceholderValue + SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_END, ddAttributeName);
294 
295             if (rebuildDDTypes) {
296                 buildDataDictionarySchemaValidationType(ddAttributeName, typesSchemaLines, builtTypes);
297             }
298         }
299         else {
300             if (LOG.isDebugEnabled()) {
301                 LOG.debug("Setting validation to use type: " + xsdValidation);
302             }
303             outLine = StringUtils.replace(outLine, SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_BEGIN + orignalPlaceholderValue + SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_END, xsdValidation);
304         }
305 
306         return outLine;
307     }
308 
309     /**
310      * Constructs new AttributeSchemaValidationBuilder for the given attribute name to build the type XML lines which are added to
311      * the given collection
312      * 
313      * @param ddAttributeName attribute entry name (business object class and attribute name) with dd: namespace prefix
314      * @param typesSchemaLines collection of type XML lines to add to for any new types
315      * @param builtTypes - Set of attribute names for which a schema validation type has been built
316      * @see org.kuali.ole.sys.context.AttributeSchemaValidationBuilder
317      */
318     protected static void buildDataDictionarySchemaValidationType(String ddAttributeName, Collection typesSchemaLines, Set<String> builtTypes) {
319         // strip prefix from attribute name so we can find it in dd map
320         String attributeEntryName = StringUtils.removeStart(ddAttributeName, DD_VALIDATION_PREFIX);
321         if (LOG.isDebugEnabled()) {
322             LOG.debug("Retrieving entry from data dictionary for attribute: " + attributeEntryName);
323         }
324 
325         // only build one type for the attribute name
326         if (!builtTypes.contains(attributeEntryName)) {
327             AttributeSchemaValidationBuilder schemaBuilder = new AttributeSchemaValidationBuilder(attributeEntryName);
328             typesSchemaLines.addAll(schemaBuilder.toSchemaType());
329             typesSchemaLines.add(" ");
330 
331             builtTypes.add(attributeEntryName);
332         }
333     }
334 
335     /**
336      * Builds header XML lines for the data dictionary types include
337      * 
338      * @return Collection containing the XML lines
339      */
340     protected static Collection initalizeDataDictionaryTypesSchema() {
341         LOG.debug("Initializing dd types schema");
342         Collection typesSchemaLines = new ArrayList();
343 
344         typesSchemaLines.add("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
345         typesSchemaLines.add("<xsd:schema elementFormDefault=\"qualified\"");
346         typesSchemaLines.add("    targetNamespace=\"http://www.kuali.org/ole/sys/ddTypes\"");
347         typesSchemaLines.add("    xmlns:dd=\"http://www.kuali.org/ole/sys/ddTypes\"");
348         typesSchemaLines.add("    xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">");
349         typesSchemaLines.add("");
350 
351         return typesSchemaLines;
352     }
353 
354     /**
355      * Builds footer XML lines for the data dictionary types include .
356      * 
357      * @return Collection containing the XML lines
358      */
359     protected static Collection finalizeDataDictionaryTypesSchema() {
360         LOG.debug("Finalizing dd types schema");
361         Collection typesSchemaLines = new ArrayList();
362 
363         typesSchemaLines.add("</xsd:schema>");
364 
365         return typesSchemaLines;
366     }
367 
368     /**
369      * Determines what the relative path of the given file is relative to the given parent path. Since parentPath is configured
370      * string method checks for / or \\ path separators .
371      * 
372      * <pre>
373      * eg. File path - /build/project/xsd/gl/collector.xsd, Parent Path - /build/project/xsd returns gl/collector.xsd
374      * </pre>
375      * 
376      * @param file File for which we want to find the relative path
377      * @param parentPath Path to parent directory
378      * @return String the relative path of the file
379      */
380     protected static String getRelativeFilePathName(File file, String parentPath) {
381         // create File for parentPath so we can compare path to schema File
382         File parentDirectory = new File(parentPath);
383 
384         String fullParentPathName = parentDirectory.getAbsolutePath();
385         String fullFilePathName = file.getAbsolutePath();
386 
387         String relativeFilePathName = StringUtils.substringAfter(fullFilePathName, fullParentPathName);
388         if (LOG.isDebugEnabled()) {
389             LOG.debug("sub-directory for schema: " + relativeFilePathName);
390         }
391 
392         if (StringUtils.isBlank(relativeFilePathName)) {
393             String msg = String.format("Cannot find relative path for file name %s from parent directory %s", fullFilePathName, fullParentPathName);
394             LOG.error(msg);
395             throw new RuntimeException(msg);
396         }
397 
398         return relativeFilePathName;
399     }
400 
401     /**
402      * Helper method for logging an error and throwing a new RuntimeException
403      * 
404      * @param msg message for logging and exception
405      */
406     protected static void logAndThrowException(String msg) {
407         LOG.error(msg);
408         throw new RuntimeException(msg);
409     }
410 
411 }