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