001/*
002 * Copyright 2010 The Kuali Foundation.
003 * 
004 * Licensed under the Educational Community License, Version 1.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.opensource.org/licenses/ecl1.php
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.kuali.ole.sys.context;
017
018import static org.kuali.ole.sys.OLEConstants.SchemaBuilder.DD_VALIDATION_PREFIX;
019import static org.kuali.ole.sys.OLEConstants.SchemaBuilder.SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_BEGIN;
020import static org.kuali.ole.sys.OLEConstants.SchemaBuilder.SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_END;
021import static org.kuali.ole.sys.OLEConstants.SchemaBuilder.XSD_VALIDATION_PREFIX;
022
023import java.io.File;
024import java.io.IOException;
025import java.util.ArrayList;
026import java.util.Collection;
027import java.util.HashSet;
028import java.util.Iterator;
029import java.util.Set;
030
031import org.apache.commons.io.FileUtils;
032import org.apache.commons.lang.StringUtils;
033import org.apache.log4j.BasicConfigurator;
034import org.apache.log4j.Level;
035import org.apache.log4j.Logger;
036
037/**
038 * Called during the build process to output schema files with validation built from the data dictionary or set to defaults
039 */
040public class SchemaBuilder {
041    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(SchemaBuilder.class);
042    private static Level logLevel = Level.INFO;
043
044    /**
045     * <pre>
046     * Performs schema build process. 
047     * 
048     * Build directory path containing the schema files, static directory that schema files will be
049     * outputted to, and flag for whether to use data dictionary validation all must given as arguments. 
050     * 
051     * Schema files in build directory should contain place-holders for which the validation will be substituted. The place-holder begin symbol is ${,
052     * and the end symbol is }. Then the place-holder should contain two parts, first the xsd type to use if data dictionary
053     * validation is not on. The second is the data dictionary entry (businessObjectEntry.attributeName) prefixed with 'dd:' that
054     * will be pulled for dd validation. The parts should be separated with a comma. Any type values without a place-holder will
055     * not be modified. 
056     * 
057     * Program also fills in externalizable.static.content.url place-holder. Value to set should be passed as the fourth program argument
058     * </pre>
059     * 
060     * @param args
061     */
062    public static void main(String[] args) {
063        if (args.length < 5) {
064            System.err.println("ERROR: You must pass the build directory, static directory, dd flag, external content url, and rebuild types flag as arguments");
065            System.exit(8);
066        }
067        try {
068            // initialize log4j
069            BasicConfigurator.configure();
070            Logger.getRootLogger().setLevel(Level.WARN);
071            LOG.setLevel(logLevel);
072
073            String buildDirectoryPath = args[0];
074            if (StringUtils.isBlank(buildDirectoryPath)) {
075                logAndThrowException("Build directory must be passed as first argument");
076            }
077            if (LOG.isDebugEnabled()) {
078                LOG.debug("Build directory set to " + buildDirectoryPath);
079            }
080
081            String staticDirectoryPath = args[1];
082            if (StringUtils.isBlank(staticDirectoryPath)) {
083                logAndThrowException("Static directory must be passed as second argument");
084            }
085            if (LOG.isDebugEnabled()) {
086                LOG.debug("Static directory set to " + staticDirectoryPath);
087            }
088
089            String dataDictionaryValidation = args[2];
090            if (StringUtils.isBlank(dataDictionaryValidation)) {
091                logAndThrowException("Use data dictionary validation must be passed as third argument");
092            }
093
094            String externalizableContentUrl = args[3];
095            if (StringUtils.isBlank(externalizableContentUrl)) {
096                logAndThrowException("Externalizalbe static content URL must be passed as fourth argument");
097            }
098
099            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}