001    /*
002     * Copyright 2011 The Kuali Foundation.
003     * 
004     * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.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     */
016    package org.kuali.kfs.sys.context;
017    
018    import static org.kuali.kfs.sys.KFSConstants.SchemaBuilder.DD_VALIDATION_PREFIX;
019    import static org.kuali.kfs.sys.KFSConstants.SchemaBuilder.SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_BEGIN;
020    import static org.kuali.kfs.sys.KFSConstants.SchemaBuilder.SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_END;
021    import static org.kuali.kfs.sys.KFSConstants.SchemaBuilder.XSD_VALIDATION_PREFIX;
022    
023    import java.io.File;
024    import java.io.IOException;
025    import java.util.ArrayList;
026    import java.util.Collection;
027    import java.util.HashSet;
028    import java.util.Iterator;
029    import java.util.Set;
030    
031    import org.apache.commons.io.FileUtils;
032    import org.apache.commons.lang.StringUtils;
033    import org.apache.log4j.BasicConfigurator;
034    import org.apache.log4j.ConsoleAppender;
035    import org.apache.log4j.Level;
036    import org.apache.log4j.Logger;
037    
038    /**
039     * Called during the build process to output schema files with validation built from the data dictionary or set to defaults
040     */
041    public class SchemaBuilder {
042        private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(SchemaBuilder.class);
043        private static Level logLevel = Level.INFO;
044    
045        /**
046         * <pre>
047         * Performs schema build process. 
048         * 
049         * Build directory path containing the schema files, static directory that schema files will be
050         * outputted to, and flag for whether to use data dictionary validation all must given as arguments. 
051         * 
052         * Schema files in build directory should contain place-holders for which the validation will be substituted. The place-holder begin symbol is ${,
053         * and the end symbol is }. Then the place-holder should contain two parts, first the xsd type to use if data dictionary
054         * validation is not on. The second is the data dictionary entry (businessObjectEntry.attributeName) prefixed with 'dd:' that
055         * will be pulled for dd validation. The parts should be separated with a comma. Any type values without a place-holder will
056         * not be modified. 
057         * 
058         * Program also fills in externalizable.static.content.url place-holder. Value to set should be passed as the fourth program argument
059         * </pre>
060         * 
061         * @param args
062         */
063        public static void main(String[] args) {
064            if (args.length < 5) {
065                System.err.println("ERROR: You must pass the build directory, static directory, dd flag, external content url, and rebuild types flag as arguments");
066                System.exit(8);
067            }
068            try {
069                // initialize log4j
070                BasicConfigurator.configure();
071                Logger.getRootLogger().setLevel(Level.WARN);
072                LOG.setLevel(logLevel);
073    
074                String buildDirectoryPath = args[0];
075                if (StringUtils.isBlank(buildDirectoryPath)) {
076                    logAndThrowException("Build directory must be passed as first argument");
077                }
078                LOG.debug("Build directory set to " + buildDirectoryPath);
079    
080                String staticDirectoryPath = args[1];
081                if (StringUtils.isBlank(staticDirectoryPath)) {
082                    logAndThrowException("Static directory must be passed as second argument");
083                }
084                LOG.debug("Static directory set to " + staticDirectoryPath);
085    
086                String dataDictionaryValidation = args[2];
087                if (StringUtils.isBlank(dataDictionaryValidation)) {
088                    logAndThrowException("Use data dictionary validation must be passed as third argument");
089                }
090    
091                String externalizableContentUrl = args[3];
092                if (StringUtils.isBlank(externalizableContentUrl)) {
093                    logAndThrowException("Externalizalbe static content URL must be passed as fourth argument");
094                }
095    
096                String rebuildDDTypesFlag = args[4];
097                if (StringUtils.isBlank(rebuildDDTypesFlag)) {
098                    logAndThrowException("Rebuild DD flags must be passed as fifth argument");
099                }
100    
101                boolean useDataDictionaryValidation = Boolean.parseBoolean(dataDictionaryValidation);
102                boolean rebuildDDTypes = Boolean.parseBoolean(rebuildDDTypesFlag);
103                LOG.debug("Use data dictionary validation set to " + useDataDictionaryValidation);
104    
105                // if using dd validation must start up spring so we can read DD
106                if (useDataDictionaryValidation && rebuildDDTypes) {
107                    SpringContext.initializeApplicationContextWithoutSchedule();
108                }
109    
110                LOG.debug("Getting build schema files");
111                Collection buildSchemaFiles = getBuildSchemaFiles(buildDirectoryPath);
112    
113                LOG.debug("Building schema files");
114                try {
115                    buildSchemaFiles(buildSchemaFiles, staticDirectoryPath, buildDirectoryPath, useDataDictionaryValidation, externalizableContentUrl, rebuildDDTypes);
116                }
117                catch (IOException ex) {
118                    LOG.error("Error building schema files: " + ex.getMessage(), ex);
119                    throw new RuntimeException("Error building schema files: " + ex.getMessage(), ex);
120                }
121    
122                LOG.info("Finished building schema files.");
123                System.exit(0);
124            }
125            catch (Throwable t) {
126                System.err.println("ERROR: Exception caught: " + t.getMessage());
127                t.printStackTrace(System.err);
128                System.exit(8);
129            }
130        }
131    
132        /**
133         * Returns Collection of File objects for all .xsd files found in given directory (including sub-directories)
134         * 
135         * @param buildDirectoryPath Directory to look for schema files
136         * @return Collection of File objects
137         */
138        protected static Collection getBuildSchemaFiles(String buildDirectoryPath) {
139            File buildDirectory = new File(buildDirectoryPath);
140    
141            return FileUtils.listFiles(buildDirectory, new String[] { "xsd" }, true);
142        }
143    
144        /**
145         * Iterates through build schema files processing validation place-holders and outputting to static directory. Include file
146         * for referenced schema types is also written out
147         * 
148         * @param buildSchemaFiles collection of File objects for build schema files
149         * @param staticDirectoryPath path that processed schema files will be written to
150         * @param buildDirectoryPath path of build schema files
151         * @param useDataDictionaryValidation indicates whether data dictionary validation should be used, if false the general xsd
152         *            datatype in the place-holder will be used
153         * @param externalizableContentUrl URL to set for externalizable.static.content.url token
154         * @throws IOException thrown for any read/write errors encountered
155         */
156        protected static void buildSchemaFiles(Collection buildSchemaFiles, String staticDirectoryPath, String buildDirectoryPath, boolean useDataDictionaryValidation, String externalizableContentUrl, boolean rebuildDDTypes) throws IOException {
157            // initialize dd type schema
158            Collection typesSchemaLines = initalizeDataDictionaryTypesSchema();
159            Set<String> builtTypes = new HashSet<String>();
160    
161            // convert static directory path to abstract path
162            File staticDirectory = new File(staticDirectoryPath);
163            String staticPathName = staticDirectory.getAbsolutePath();
164    
165            for (Iterator iterator = buildSchemaFiles.iterator(); iterator.hasNext();) {
166                File buildSchemFile = (File) iterator.next();
167                LOG.debug("Processing schema file: " + buildSchemFile.getName());
168    
169                String outSchemaFilePathName = staticPathName + getRelativeFilePathName(buildSchemFile, buildDirectoryPath);
170                LOG.info("Building schema file: " + outSchemaFilePathName);
171    
172                buildSchemaFile(buildSchemFile, outSchemaFilePathName, useDataDictionaryValidation, typesSchemaLines, externalizableContentUrl, builtTypes, rebuildDDTypes);
173            }
174    
175            // finalize dd type schema
176            typesSchemaLines.addAll(finalizeDataDictionaryTypesSchema());
177    
178            if (rebuildDDTypes) {
179                LOG.debug("Writing ddTypes schema file");
180                File ddTypesFile = new File(staticPathName + File.separator + "xsd" + File.separator + "sys" + File.separator + "ddTypes.xsd");
181                File ddTypesFileBuild = new File(buildDirectoryPath + File.separator + "xsd" + File.separator + "sys" + File.separator + "ddTypes.xsd");
182                FileUtils.writeLines(ddTypesFile, typesSchemaLines);
183                FileUtils.copyFile(ddTypesFile, ddTypesFileBuild);
184            }
185        }
186    
187        /**
188         * Process a single schema file (setting validation and externalizable token) and outputs to static directory. Any new data
189         * dictionary types encountered are added to the given Collection for later writing to the types include file
190         * 
191         * @param buildSchemFile build schema file that should be processed
192         * @param outSchemaFilePathName full file path name for the outputted schema
193         * @param useDataDictionaryValidation indicates whether data dictionary validation should be used, if false the general xsd
194         *            datatype in the place-holder will be used
195         * @param typesSchemaLines collection of type XML lines to add to for any new types
196         * @param externalizableContentUrl URL to set for externalizable.static.content.url token
197         * @param builtTypes - Set of attribute names for which a schema validation type has been built
198         * @throws IOException thrown for any read/write errors encountered
199         */
200        protected static void buildSchemaFile(File buildSchemFile, String outSchemaFilePathName, boolean useDataDictionaryValidation, Collection typesSchemaLines, String externalizableContentUrl, Set<String> builtTypes, boolean rebuildDDTypes) throws IOException {
201            Collection buildSchemaLines = FileUtils.readLines(buildSchemFile);
202            Collection outSchemaLines = new ArrayList();
203            int lineCount = 1;
204            for (Iterator iterator = buildSchemaLines.iterator(); iterator.hasNext();) {
205                LOG.debug("Processing line " + lineCount + "of file " + buildSchemFile.getAbsolutePath());
206                String buildLine = (String) iterator.next();
207                String outLine = buildLine;
208    
209                // check for externalizable.static.content.url token and replace if found
210                if (StringUtils.contains(buildLine, "@externalizable.static.content.url@")) {
211                    outLine = StringUtils.replace(buildLine, "@externalizable.static.content.url@", externalizableContentUrl);
212                }
213    
214                // check for validation place-holder and process if found
215                else if (StringUtils.contains(buildLine, SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_BEGIN) && StringUtils.contains(buildLine, SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_END)) {
216                    String validationPlaceholder = StringUtils.substringBetween(buildLine, SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_BEGIN, SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_END);
217                    if (StringUtils.isBlank(validationPlaceholder)) {
218                        logAndThrowException(String.format("File %s line %s: validation placeholder cannot be blank", buildSchemFile.getAbsolutePath(), lineCount));
219                    }
220    
221                    LOG.debug("Found dd validation placeholder: " + validationPlaceholder);
222                    if (!StringUtils.contains(validationPlaceholder, ",")) {
223                        logAndThrowException(String.format("File %s, line %s: Invalid format of placehoder value: %s, must contain a ',' seperating parts", buildSchemFile.getAbsolutePath(), lineCount, validationPlaceholder));
224                    }
225    
226                    outLine = processValidationPlaceholder(validationPlaceholder, buildLine, buildSchemFile.getAbsolutePath(), lineCount, useDataDictionaryValidation, typesSchemaLines, builtTypes, rebuildDDTypes);
227                }
228    
229                outSchemaLines.add(outLine);
230                lineCount++;
231            }
232    
233            LOG.debug("Writing schema file to static directory");
234            File schemaFile = new File(outSchemaFilePathName);
235            FileUtils.writeLines(schemaFile, outSchemaLines);
236        }
237    
238        /**
239         * Performs logic to processes a validation place-holder for a line. First collects the configuration given in the
240         * place-holder (general xsd type and data dictionary attribute). If use data dictionary validation is set to false, then the
241         * place-holder will be set to the xsd type. If data dictionary validation is set to true, the general xsd type will be
242         * removed, and the corresponding the data dictionary will be consulted to build the dd type
243         * 
244         * <pre>
245         * ex. type="${xsd:token,dd:Chart.chartOfAccountsCode}" with useDataDictionaryValidation=false becomes type="xsd:token"
246         *    type="${xsd:token,dd:Chart.chartOfAccountsCode}" with useDataDictionaryValidation=true becomes type="dd:Chart.chartOfAccountsCode" and XML lines created for dd Types file
247         * </pre>
248         * 
249         * @param validationPlaceholder the parsed place-holder contents
250         * @param buildLine the complete line being read
251         * @param fileName the name for the file being processed
252         * @param lineCount count for the line being read
253         * @param useDataDictionaryValidation indicates whether data dictionary validation should be used, if false the general xsd
254         *            datatype in the place-holder will be used
255         * @param typesSchemaLines collection of type XML lines to add to for any new types
256         * @param builtTypes - Set of attribute names for which a schema validation type has been built
257         * @return String the out XML line (which validation filled in)
258         */
259        protected static String processValidationPlaceholder(String validationPlaceholder, String buildLine, String fileName, int lineCount, boolean useDataDictionaryValidation, Collection typesSchemaLines, Set<String> builtTypes, boolean rebuildDDTypes) {
260            String orignalPlaceholderValue = validationPlaceholder;
261    
262            // remove whitespace
263            validationPlaceholder = StringUtils.deleteWhitespace(validationPlaceholder);
264    
265            // get two parts of validation place-holder
266            String[] validationParts = StringUtils.split(validationPlaceholder, ",");
267            String xsdValidation = validationParts[0];
268            if (StringUtils.isBlank(xsdValidation) || !xsdValidation.startsWith(XSD_VALIDATION_PREFIX)) {
269                logAndThrowException(String.format("File %s, line %s: specified xsd validation is invalid, must start with %s", fileName, lineCount, XSD_VALIDATION_PREFIX));
270            }
271    
272            String ddAttributeName = validationParts[1];
273            if (StringUtils.isBlank(ddAttributeName) || !ddAttributeName.startsWith(DD_VALIDATION_PREFIX)) {
274                logAndThrowException(String.format("File %s, line %s: specified dd validation is invalid, must start with %s", fileName, lineCount, DD_VALIDATION_PREFIX));
275            }
276    
277            String outLine = buildLine;
278            if (useDataDictionaryValidation) {
279                LOG.debug("Setting validation to use type: " + ddAttributeName);
280                outLine = StringUtils.replace(outLine, SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_BEGIN + orignalPlaceholderValue + SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_END, ddAttributeName);
281    
282                if (rebuildDDTypes) {
283                    buildDataDictionarySchemaValidationType(ddAttributeName, typesSchemaLines, builtTypes);
284                }
285            }
286            else {
287                LOG.debug("Setting validation to use type: " + xsdValidation);
288                outLine = StringUtils.replace(outLine, SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_BEGIN + orignalPlaceholderValue + SCHEMA_FILE_DD_VALIDATION_PLACEHOLDER_END, xsdValidation);
289            }
290    
291            return outLine;
292        }
293    
294        /**
295         * Constructs new AttributeSchemaValidationBuilder for the given attribute name to build the type XML lines which are added to
296         * the given collection
297         * 
298         * @param ddAttributeName attribute entry name (business object class and attribute name) with dd: namespace prefix
299         * @param typesSchemaLines collection of type XML lines to add to for any new types
300         * @param builtTypes - Set of attribute names for which a schema validation type has been built
301         * @see org.kuali.kfs.sys.context.AttributeSchemaValidationBuilder
302         */
303        protected static void buildDataDictionarySchemaValidationType(String ddAttributeName, Collection typesSchemaLines, Set<String> builtTypes) {
304            // strip prefix from attribute name so we can find it in dd map
305            String attributeEntryName = StringUtils.removeStart(ddAttributeName, DD_VALIDATION_PREFIX);
306            LOG.debug("Retrieving entry from data dictionary for attribute: " + attributeEntryName);
307    
308            // only build one type for the attribute name
309            if (!builtTypes.contains(attributeEntryName)) {
310                AttributeSchemaValidationBuilder schemaBuilder = new AttributeSchemaValidationBuilder(attributeEntryName);
311                typesSchemaLines.addAll(schemaBuilder.toSchemaType());
312                typesSchemaLines.add(" ");
313    
314                builtTypes.add(attributeEntryName);
315            }
316        }
317    
318        /**
319         * Builds header XML lines for the data dictionary types include
320         * 
321         * @return Collection containing the XML lines
322         */
323        protected static Collection initalizeDataDictionaryTypesSchema() {
324            LOG.debug("Initializing dd types schema");
325            Collection typesSchemaLines = new ArrayList();
326    
327            typesSchemaLines.add("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
328            typesSchemaLines.add("<xsd:schema elementFormDefault=\"qualified\"");
329            typesSchemaLines.add("    targetNamespace=\"http://www.kuali.org/kfs/sys/ddTypes\"");
330            typesSchemaLines.add("    xmlns:dd=\"http://www.kuali.org/kfs/sys/ddTypes\"");
331            typesSchemaLines.add("    xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">");
332            typesSchemaLines.add("");
333    
334            return typesSchemaLines;
335        }
336    
337        /**
338         * Builds footer XML lines for the data dictionary types include .
339         * 
340         * @return Collection containing the XML lines
341         */
342        protected static Collection finalizeDataDictionaryTypesSchema() {
343            LOG.debug("Finalizing dd types schema");
344            Collection typesSchemaLines = new ArrayList();
345    
346            typesSchemaLines.add("</xsd:schema>");
347    
348            return typesSchemaLines;
349        }
350    
351        /**
352         * Determines what the relative path of the given file is relative to the given parent path. Since parentPath is configured
353         * string method checks for / or \\ path separators .
354         * 
355         * <pre>
356         * eg. File path - /build/project/xsd/gl/collector.xsd, Parent Path - /build/project/xsd returns gl/collector.xsd
357         * </pre>
358         * 
359         * @param file File for which we want to find the relative path
360         * @param parentPath Path to parent directory
361         * @return String the relative path of the file
362         */
363        protected static String getRelativeFilePathName(File file, String parentPath) {
364            // create File for parentPath so we can compare path to schema File
365            File parentDirectory = new File(parentPath);
366    
367            String fullParentPathName = parentDirectory.getAbsolutePath();
368            String fullFilePathName = file.getAbsolutePath();
369    
370            String relativeFilePathName = StringUtils.substringAfter(fullFilePathName, fullParentPathName);
371            LOG.debug("sub-directory for schema: " + relativeFilePathName);
372    
373            if (StringUtils.isBlank(relativeFilePathName)) {
374                String msg = String.format("Cannot find relative path for file name %s from parent directory %s", fullFilePathName, fullParentPathName);
375                LOG.error(msg);
376                throw new RuntimeException(msg);
377            }
378    
379            return relativeFilePathName;
380        }
381    
382        /**
383         * Helper method for logging an error and throwing a new RuntimeException
384         * 
385         * @param msg message for logging and exception
386         */
387        protected static void logAndThrowException(String msg) {
388            LOG.error(msg);
389            throw new RuntimeException(msg);
390        }
391    
392    }