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}