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 }