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    }