001 /* 002 * Copyright 2011 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 */ 016 package org.kuali.student.datadictionary.mojo; 017 018 import java.io.File; 019 import java.io.FileNotFoundException; 020 import java.io.FileOutputStream; 021 import java.io.OutputStream; 022 import java.io.PrintStream; 023 import java.net.MalformedURLException; 024 import java.net.URL; 025 import java.net.URLClassLoader; 026 import java.util.ArrayList; 027 import java.util.Collection; 028 import java.util.LinkedHashMap; 029 import java.util.LinkedHashSet; 030 import java.util.List; 031 import java.util.Map; 032 import java.util.Set; 033 034 import org.apache.maven.artifact.DependencyResolutionRequiredException; 035 import org.apache.maven.plugin.AbstractMojo; 036 import org.apache.maven.plugin.MojoExecutionException; 037 import org.apache.maven.project.MavenProject; 038 import org.joda.time.DateTime; 039 import org.kuali.student.contract.model.MessageStructure; 040 import org.kuali.student.contract.model.ServiceContractModel; 041 import org.kuali.student.contract.model.impl.ServiceContractModelCache; 042 import org.kuali.student.contract.model.impl.ServiceContractModelQDoxLoader; 043 import org.kuali.student.contract.model.util.DateUtility; 044 import org.kuali.student.contract.model.util.VersionLinesUtility; 045 import org.kuali.student.contract.model.validation.ServiceContractModelValidator; 046 import org.kuali.student.datadictionary.util.DictionaryFormatter; 047 import org.kuali.student.datadictionary.util.DictionaryTesterHelper; 048 import org.slf4j.Logger; 049 import org.slf4j.LoggerFactory; 050 051 /** 052 * Mojo for generating a formatted view of the data dictionary. 053 * 054 * <pre> 055 * {@code 056 * <plugin> 057 * <groupId>org.kuali.maven.plugins</groupId> 058 * <artifactId>maven-kscontractdoc-plugin</artifactId> 059 * <execution> 060 * <id>generate-dictionary-documentation</id> 061 * <phase>site</phase> 062 * <goals> 063 * <goal>ksdictionarydoc</goal> 064 * </goals> 065 * <configuration> 066 * <supportFiles> 067 * <supportFile>commonApplicationContext.xml</supportFile> 068 * </supportFiles> 069 * </configuration> 070 * </execution> 071 * </plugin> 072 * } 073 * </pre> 074 * 075 * We use the QDox model to read the class files present and to enumerate the list of Message Structure objects. Then for each identified type we build an application context that includes the union of that 076 * file plus any files specified using the <supportFile> configuration parameter. 077 * 078 * Errors with an application context are detected and logged but will not break the plugin's ability to generate the other files. 079 * 080 * @goal ksdictionarydoc 081 * @phase site 082 * @requiresDependencyResolution test 083 */ 084 public class KSDictionaryDocMojo extends AbstractMojo { 085 086 private static final Logger log = LoggerFactory.getLogger(KSDictionaryDocMojo.class); 087 088 /** 089 * @parameter expression="${project}" 090 * @required 091 * @readonly 092 */ 093 private MavenProject project; 094 095 /** 096 * @parameter 097 **/ 098 private List<String> sourceDirs; 099 100 /** 101 * The base applicationContext files. 102 * @parameter 103 **/ 104 private List<String> supportFiles = new ArrayList<String>(); 105 /** 106 * @parameter expression="${htmlDirectory}" default-value="${project.build.directory}/site/services/dictionarydocs" 107 */ 108 private File htmlDirectory; 109 110 private String testDictionaryFile; 111 112 private LinkedHashMap<String, String> dictionaryFileToMessageStructureMap = new LinkedHashMap<String, String>(); 113 114 115 public List<String> getSourceDirs() { 116 return sourceDirs; 117 } 118 119 public void setSourceDirs(List<String> sourceDirs) { 120 this.sourceDirs = sourceDirs; 121 } 122 123 public void setHtmlDirectory(File htmlDirectory) { 124 this.htmlDirectory = htmlDirectory; 125 } 126 127 public File getHtmlDirectory() { 128 return htmlDirectory; 129 } 130 131 public MavenProject getProject() { 132 return project; 133 } 134 135 public List<String> getSupportFiles() { 136 return supportFiles; 137 } 138 139 public void setSupportFiles(List<String> supportFiles) { 140 this.supportFiles.clear(); 141 142 if (supportFiles != null) 143 this.supportFiles.addAll(supportFiles); 144 } 145 146 private ServiceContractModel getModel() { 147 ServiceContractModel instance = new ServiceContractModelQDoxLoader( 148 sourceDirs); 149 return new ServiceContractModelCache(instance); 150 } 151 152 private boolean validate(ServiceContractModel model) { 153 Collection<String> errors = new ServiceContractModelValidator(model).validate(); 154 if (errors.size() > 0) { 155 StringBuilder buf = new StringBuilder(); 156 buf.append(errors.size()).append(" errors found while validating the data."); 157 return false; 158 } 159 return true; 160 } 161 162 @Override 163 public void execute() 164 throws MojoExecutionException { 165 this.getLog().info("generating dictionary documentation"); 166 167 if (getPluginContext() != null) { 168 project = (MavenProject) getPluginContext().get("project"); 169 } 170 171 // add the current projects classpath to the plugin so the springbean 172 // loader can find the xml files and lasses that it needs to can be run 173 // against the current project's files 174 if (project != null) { 175 this.getLog().info("adding current project's classpath to plugin class loader"); 176 List<String> runtimeClasspathElements; 177 try { 178 runtimeClasspathElements = project.getRuntimeClasspathElements(); 179 } catch (DependencyResolutionRequiredException ex) { 180 throw new MojoExecutionException("Failed to get runtime classpath elements.", ex); 181 } 182 URL[] runtimeUrls = new URL[runtimeClasspathElements.size()]; 183 for (int i = 0; i < runtimeClasspathElements.size(); i++) { 184 String element = (String) runtimeClasspathElements.get(i); 185 try { 186 runtimeUrls[i] = new File(element).toURI().toURL(); 187 } catch (MalformedURLException ex) { 188 throw new MojoExecutionException(element, ex); 189 } 190 } 191 URLClassLoader newLoader = new URLClassLoader(runtimeUrls, 192 Thread.currentThread().getContextClassLoader()); 193 Thread.currentThread().setContextClassLoader(newLoader); 194 } 195 196 197 if (!htmlDirectory.exists()) { 198 if (!htmlDirectory.mkdirs()) { 199 throw new IllegalArgumentException("Could not create directory " 200 + this.htmlDirectory.getPath()); 201 } 202 } 203 204 Set<String> inpFiles = new LinkedHashSet<String>(); 205 if (project != null) { 206 ServiceContractModel model = this.getModel(); 207 this.validate(model); 208 inpFiles.addAll(extractDictionaryFiles(model)); 209 210 } 211 else { 212 inpFiles.add(this.testDictionaryFile); 213 } 214 215 216 String outputDir = this.htmlDirectory.getAbsolutePath(); 217 DictionaryTesterHelper tester = new DictionaryTesterHelper(outputDir, inpFiles, this.supportFiles); 218 tester.doTest(project.getVersion(), DateUtility.asYMDHMInEasternTimeZone(new DateTime())); 219 220 // write out the index file 221 String indexFileName = this.htmlDirectory.getPath() + "/" + "index.html"; 222 File indexFile = new File(indexFileName); 223 OutputStream outputStream; 224 try { 225 outputStream = new FileOutputStream(indexFile, false); 226 } catch (FileNotFoundException ex) { 227 // throw new MojoExecutionException(indexFileName, ex); 228 throw new IllegalArgumentException(indexFileName, ex); 229 } 230 231 String formattedDate = DateUtility.asYMDHMInEasternTimeZone(new DateTime()); 232 233 PrintStream out = new PrintStream(outputStream); 234 235 DictionaryFormatter.writeHeader(out, "Data Dictionary Index"); 236 237 VersionLinesUtility.writeVersionTag(out, "<a href=\"index.html\">Home</a>", "<a href=\"../contractdocs/index.html\">Contract Docs Home</a>", project.getVersion(), formattedDate); 238 239 out.println("<h1>Data Dictionary Index</h1>"); 240 out.println("<blockquote>A Red background indicates that there is a problem with the data dictionary for that type.</blockquote>"); 241 out.println("<ul>"); 242 243 Map<String, List<String>> fileToBeanNameMap = tester.getInputFileToBeanNameMap(); 244 245 for (String inputFile : fileToBeanNameMap.keySet()) { 246 247 boolean containsError = false; 248 249 if (tester.getInvalidDictionaryFiles().contains(inputFile)) { 250 containsError = true; 251 } 252 253 List<String> beanIds = fileToBeanNameMap.get(inputFile); 254 255 for (String beanId : beanIds) { 256 257 String outputFileName = beanId + ".html"; 258 259 if (containsError) 260 out.println ("<li class=\"invalid\">"); 261 else 262 out.println ("<li>"); 263 264 out.println("<a href=\"" + outputFileName + "\">" + beanId 265 + "</a>"); 266 } 267 } 268 out.println("</ul>"); 269 270 271 272 if (tester.getMissingDictionaryFiles().size() > 0) { 273 out.println("<h1>Missing Dictionary Files</h1>"); 274 out.println("<blockquote>The Message structure exists but there is no dictionary file present.</blockquote>"); 275 out.println("<ul>"); 276 for (String missingFile : tester.getMissingDictionaryFiles()) { 277 out.println("<li><b>" + missingFile + "</b></li>"); 278 } 279 out.println("</ul>"); 280 281 } 282 283 DictionaryFormatter.writeFooter(out); 284 out.close(); 285 286 log.info("finished generating dictionary documentation"); 287 } 288 289 private Collection<String> extractDictionaryFiles( 290 ServiceContractModel model) { 291 292 Set<String> dictionaryFiles = new LinkedHashSet<String>(); 293 294 List<MessageStructure> ms = model.getMessageStructures(); 295 296 for (MessageStructure messageStructure : ms) { 297 298 String inputFileName = "ks-" + messageStructure.getXmlObject() + "-dictionary.xml"; 299 300 dictionaryFiles.add(inputFileName); 301 302 // we also track the file name to message structure so we can link the invalid 303 dictionaryFileToMessageStructureMap.put(inputFileName, messageStructure.getXmlObject()); 304 305 } 306 307 308 return dictionaryFiles; 309 } 310 311 /** 312 * Used for testing to hard code a single dictionary file to use. 313 * @param string 314 */ 315 public void setTestDictionaryFile(String dictionaryFile) { 316 this.testDictionaryFile = dictionaryFile; 317 318 } 319 }