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 }