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 }