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.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 expression="${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 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            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    }