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 }