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 */
016package org.kuali.student.datadictionary.mojo;
017
018import java.io.File;
019import java.io.FileNotFoundException;
020import java.io.FileOutputStream;
021import java.io.OutputStream;
022import java.io.PrintStream;
023import java.net.MalformedURLException;
024import java.net.URL;
025import java.net.URLClassLoader;
026import java.util.ArrayList;
027import java.util.Collection;
028import java.util.Iterator;
029import java.util.LinkedHashMap;
030import java.util.LinkedHashSet;
031import java.util.List;
032import java.util.Map;
033import java.util.Set;
034
035import org.apache.maven.artifact.DependencyResolutionRequiredException;
036import org.apache.maven.plugin.AbstractMojo;
037import org.apache.maven.plugin.MojoExecutionException;
038import org.apache.maven.project.MavenProject;
039import org.joda.time.DateTime;
040import org.kuali.student.common.mojo.AbstractKSMojo;
041import org.kuali.student.contract.model.MessageStructure;
042import org.kuali.student.contract.model.Service;
043import org.kuali.student.contract.model.ServiceContractModel;
044import org.kuali.student.contract.model.impl.ServiceContractModelCache;
045import org.kuali.student.contract.model.impl.ServiceContractModelQDoxLoader;
046import org.kuali.student.contract.model.util.DateUtility;
047import org.kuali.student.contract.model.util.VersionLinesUtility;
048import org.kuali.student.contract.model.validation.ServiceContractModelValidator;
049import org.kuali.student.datadictionary.util.DictionaryFormatter;
050import org.kuali.student.datadictionary.util.DictionaryTesterHelper;
051import org.slf4j.Logger;
052import 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 */
087public 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}