001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.camel.maven;
018    
019    import java.io.File;
020    import java.io.FileOutputStream;
021    import java.io.IOException;
022    import java.io.OutputStreamWriter;
023    import java.io.Writer;
024    import java.lang.reflect.Method;
025    import java.util.HashMap;
026    import java.util.List;
027    import java.util.Locale;
028    import java.util.Map;
029    import java.util.ResourceBundle;
030    import java.util.Set;
031    import java.util.TreeMap;
032    import java.util.TreeSet;
033    
034    import org.apache.camel.impl.DefaultPackageScanClassResolver;
035    import org.apache.camel.util.ObjectHelper;
036    import org.apache.maven.artifact.Artifact;
037    import org.apache.maven.artifact.factory.ArtifactFactory;
038    import org.apache.maven.artifact.repository.ArtifactRepository;
039    import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
040    import org.apache.maven.artifact.resolver.ArtifactResolutionException;
041    import org.apache.maven.artifact.resolver.ArtifactResolver;
042    import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
043    import org.apache.maven.artifact.versioning.VersionRange;
044    import org.apache.maven.doxia.module.xhtml.decoration.render.RenderingContext;
045    import org.apache.maven.doxia.sink.Sink;
046    import org.apache.maven.doxia.site.decoration.Body;
047    import org.apache.maven.doxia.site.decoration.DecorationModel;
048    import org.apache.maven.doxia.site.decoration.Skin;
049    import org.apache.maven.doxia.siterenderer.Renderer;
050    import org.apache.maven.doxia.siterenderer.RendererException;
051    import org.apache.maven.doxia.siterenderer.SiteRenderingContext;
052    import org.apache.maven.doxia.siterenderer.sink.SiteRendererSink;
053    import org.apache.maven.plugin.MojoExecutionException;
054    import org.apache.maven.plugin.MojoFailureException;
055    import org.apache.maven.project.MavenProject;
056    import org.apache.maven.reporting.AbstractMavenReport;
057    import org.apache.maven.reporting.MavenReportException;
058    
059    /**
060     * Generate report of available type conversions.
061     *
062     * @goal converters-report
063     * @requiresDependencyResolution runtime
064     * @phase verify
065     */
066    public class ConvertersMojo extends AbstractMavenReport {
067    
068        private static final String WIKI_TYPECONVERER_URL = "http://camel.apache.org/type-converter.html";
069        private static final String CONVERTER_TYPE_STATIC = "org.apache.camel.impl.converter.StaticMethodTypeConverter";
070        private static final String CONVERTER_TYPE_INSTANCE = "org.apache.camel.impl.converter.InstanceMethodTypeConverter";
071        private static final String REPORT_METHOD_STATIC = "STATIC";
072        private static final String REPORT_METHOD_INSTANCE = "INSTANCE";
073        private static final String REPORT_METHOD_UNKNOWN = "UNKNOWN";
074    
075        /**
076         * Remote repositories which will be searched for source attachments.
077         *
078         * @parameter expression="${project.remoteArtifactRepositories}"
079         * @required
080         * @readonly
081         */
082        protected List remoteArtifactRepositories;
083    
084        /**
085         * Local maven repository.
086         *
087         * @parameter expression="${localRepository}"
088         * @required
089         * @readonly
090         */
091        protected ArtifactRepository localRepository;
092    
093        /**
094         * The component that is used to resolve additional artifacts required.
095         *
096         * @component
097         */
098        protected ArtifactResolver artifactResolver;
099    
100        /**
101         * The component used for creating artifact instances.
102         *
103         * @component
104         */
105        protected ArtifactFactory artifactFactory;
106    
107        /**
108         * Base output directory for reports.
109         *
110         * @parameter default-value="${project.build.directory}/site"
111         * @readonly
112         * @required
113         */
114        private File outputDirectory;
115    
116        /**
117         * Reference to Maven 2 Project.
118         *
119         * @parameter expression="${project}"
120         * @required
121         * @readonly
122         */
123        private MavenProject project;
124    
125        /**
126         * Doxia SiteRenderer.
127         *
128         * @component
129         */
130        private Renderer renderer;
131    
132        /**
133         * Gets resource bundle for given locale.
134         *
135         * @param locale    locale
136         * @return resource bundle
137         */
138        protected ResourceBundle getBundle(final Locale locale) {
139            return ResourceBundle.getBundle("camel-maven-plugin", locale, this
140                    .getClass().getClassLoader());
141        }
142    
143        /**
144         * @param locale
145         *            report locale.
146         * @return report description.
147         * @see org.apache.maven.reporting.MavenReport#getDescription(Locale)
148         */
149        public String getDescription(final Locale locale) {
150            return getBundle(locale).getString("report.converters.description");
151        }
152    
153        /**
154         * @see org.apache.maven.reporting.MavenReport#getName(Locale)
155         */
156        public String getName(final Locale locale) {
157            return getBundle(locale).getString("report.converters.name");
158        }
159    
160        public String getOutputName() {
161            return "camel-converters";
162        }
163    
164        @Override
165        protected String getOutputDirectory() {
166            return outputDirectory.getAbsolutePath();
167        }
168    
169        @Override
170        protected MavenProject getProject() {
171            return this.project;
172        }
173    
174        @Override
175        protected Renderer getSiteRenderer() {
176            return renderer;
177        }
178    
179        public void execute() throws MojoExecutionException {
180            if (!canGenerateReport()) {
181                return;
182            }
183    
184            try {
185                DecorationModel model = new DecorationModel();
186                model.setBody(new Body());
187                Map<String, Object> attributes = new HashMap<String, Object>();
188                attributes.put("outputEncoding", "UTF-8");
189                attributes.put("project", project);
190                Locale locale = Locale.getDefault();
191    
192                SiteRenderingContext siteContext = renderer.createContextForSkin(
193                        getSkinArtifactFile(model), attributes, model,
194                        getName(locale), locale);
195    
196                RenderingContext context = new RenderingContext(
197                        getReportOutputDirectory(), getOutputName() + ".html");
198                SiteRendererSink sink = new SiteRendererSink(context);
199                generate(sink, locale);
200    
201                Writer writer = new OutputStreamWriter(new FileOutputStream(
202                        new File(getReportOutputDirectory(), getOutputName()
203                                + ".html")), "UTF-8");
204    
205                renderer.generateDocument(writer, sink, siteContext);
206                renderer.copyResources(siteContext, new File(project.getBasedir(),
207                        "src/site/resources"), outputDirectory);
208            } catch (IOException e) {
209                throw new MojoExecutionException("Error copying resources.", e);
210            } catch (RendererException e) {
211                throw new MojoExecutionException("Error while rendering report.", e);
212            } catch (MojoFailureException e) {
213                throw new MojoExecutionException("Cannot find skin artifact for report.", e);
214            } catch (MavenReportException e) {
215                throw new MojoExecutionException("Error generating report.", e);
216            }
217        }
218    
219        @Override
220        protected void executeReport(Locale locale) throws MavenReportException {
221    
222            if (!createOutputDirectory(outputDirectory)) {
223                throw new MavenReportException("Failed to create report directory "
224                        + outputDirectory.getAbsolutePath());
225            }
226    
227            ClassLoader oldClassLoader = Thread.currentThread()
228                    .getContextClassLoader();
229            try {
230                // TODO: this badly needs some refactoring
231                // mojo.createClassLoader creates a URLClassLoader with whatever is in
232                // ${project.testClasspathElements}, reason why we don't see all converters
233                // in the report. First we need a list of classpath elements the user
234                // could customize via plugin configuration, and elements of that list
235                // be added to the URLClassLoader. This should also be factored out into
236                // a utility class.
237                // TODO: there is some interference with the site plugin that needs investigated.
238                List<?> list = project.getTestClasspathElements();
239                EmbeddedMojo mojo = new EmbeddedMojo();
240                mojo.setClasspathElements(list);
241                ClassLoader newClassLoader = mojo.createClassLoader(oldClassLoader);
242                Thread.currentThread().setContextClassLoader(newClassLoader);
243    
244                ReportingTypeConverterLoader loader = new ReportingTypeConverterLoader(new DefaultPackageScanClassResolver());
245                ReportingTypeConverterRegistry registry = new ReportingTypeConverterRegistry();
246                loader.load(registry);
247                getLog().error("FOUND type mapping; count = " + loader.getTypeConversions().length);
248    
249                String[] errors = registry.getErrors();
250                for (String error : errors) {
251                    getLog().error(error);
252                }
253    
254                generateReport(getSink(), locale, loader.getTypeConversions());
255            } catch (Exception e) {
256                throw new MavenReportException(
257                        "Failed to generate TypeConverters report", e);
258            } finally {
259                Thread.currentThread().setContextClassLoader(oldClassLoader);
260            }
261        }
262    
263        private boolean createOutputDirectory(final File outputDir) {
264            if (outputDir.exists()) {
265                if (!outputDir.isDirectory()) {
266                    getLog().error("File with same name already exists: " + outputDir.getAbsolutePath());
267                    return false;
268                }
269            } else {
270                if (!outputDir.mkdirs()) {
271                    getLog().error("Cannot make output directory at: " + outputDir.getAbsolutePath());
272                    return false;
273                }
274            }
275            return true;
276        }
277    
278        private File getSkinArtifactFile(DecorationModel decoration) throws MojoFailureException {
279    
280            Skin skin = decoration.getSkin();
281            if (skin == null) {
282                skin = Skin.getDefaultSkin();
283            }
284    
285            String version = skin.getVersion();
286            Artifact artifact;
287            try {
288                if (version == null) {
289                    version = Artifact.RELEASE_VERSION;
290                }
291    
292                VersionRange versionSpec = VersionRange
293                        .createFromVersionSpec(version);
294                artifact = artifactFactory.createDependencyArtifact(skin
295                        .getGroupId(), skin.getArtifactId(), versionSpec, "jar",
296                        null, null);
297    
298                artifactResolver.resolve(artifact, remoteArtifactRepositories,
299                        localRepository);
300                return artifact.getFile();
301            } catch (InvalidVersionSpecificationException e) {
302                throw new MojoFailureException("The skin version '" + version
303                        + "' is not valid: " + e.getMessage());
304            } catch (ArtifactResolutionException e) {
305                throw new MojoFailureException("Unable to fink skin: "
306                        + e.getMessage());
307            } catch (ArtifactNotFoundException e) {
308                throw new MojoFailureException("The skin does not exist: "
309                        + e.getMessage());
310            }
311        }
312    
313        private String converterType(String converterClassName) {
314            if (CONVERTER_TYPE_STATIC.equals(converterClassName)) {
315                return REPORT_METHOD_STATIC;
316            } else if (CONVERTER_TYPE_INSTANCE.equals(converterClassName)) {
317                return REPORT_METHOD_INSTANCE;
318            } else {
319                return REPORT_METHOD_UNKNOWN;
320            }
321        }
322    
323        private void generateReport(Sink sink, Locale locale, ReportingTypeConverterLoader.TypeMapping[] mappings)
324            throws MojoExecutionException {
325            beginReport(sink, locale);
326    
327            Set<String> classes;
328            Map<String, Set<String>> packages = new TreeMap<String, Set<String>>();
329            Class<?> prevFrom = null;
330            Class<?> prevTo = null;
331    
332            sink.table();
333            tableHeader(sink, locale);
334    
335            for (ReportingTypeConverterLoader.TypeMapping mapping : mappings) {
336                boolean ignored = false;
337                Class<?> from = mapping.getFromType();
338                Class<?> to = mapping.getToType();
339                if (ObjectHelper.equal(from, prevFrom)
340                        && ObjectHelper.equal(to, prevTo)) {
341                    ignored = true;
342                }
343                prevFrom = from;
344                prevTo = to;
345                Method method = mapping.getMethod();
346                Class<?> methodClass = method.getDeclaringClass();
347                String packageName = methodClass.getPackage().getName();
348                if (packages.containsKey(packageName)) {
349                    classes = packages.get(packageName);
350                } else {
351                    classes = new TreeSet<String>();
352                    packages.put(packageName, classes);
353                }
354                classes.add(methodClass.getName());
355    
356                if (ignored) {
357                    sink.italic();
358                    this.tableRow(sink, from.getSimpleName(), to.getSimpleName(),
359                            method.getName(), methodClass, mapping
360                                    .getConverterType().getName());
361                    sink.italic_();
362                } else {
363                    this.tableRow(sink, from.getSimpleName(), to.getSimpleName(),
364                            method.getName(), methodClass, mapping
365                                    .getConverterType().getName());
366                }
367            }
368            sink.table_();
369    
370            generatePackageReport(sink, packages);
371    
372            endReport(sink);
373        }
374    
375        private void generatePackageReport(Sink sink,
376                Map<String, Set<String>> packages) {
377            for (Map.Entry<String, Set<String>> entry : packages.entrySet()) {
378                sink.section2();
379                sink.sectionTitle2();
380                sink.text(entry.getKey());
381                sink.sectionTitle2_();
382                sink.list();
383                for (String clazz : entry.getValue()) {
384                    sink.listItem();
385                    sink.anchor(clazz);
386                    sink.text(clazz);
387                    sink.anchor_();
388                    sink.listItem_();
389                }
390                sink.list_();
391                sink.section2_();
392            }
393        }
394    
395        private void beginReport(Sink sink, Locale locale) {
396            String title = getBundle(locale).getString(
397                    "report.converters.report.title");
398            String header = getBundle(locale).getString(
399                    "report.converters.report.header");
400            String intro = getBundle(locale).getString(
401                    "report.converters.report.intro");
402            String seealso = getBundle(locale).getString(
403                    "report.converters.report.seealso");
404    
405            sink.head();
406            sink.title();
407            sink.text(title);
408            sink.title_();
409            sink.head_();
410    
411            sink.body();
412    
413            sink.section1();
414    
415            sink.sectionTitle1();
416            sink.text(header);
417            sink.sectionTitle1_();
418    
419            sink.paragraph();
420            sink.text(intro);
421            sink.paragraph_();
422            sink.paragraph();
423            sink.text(seealso);
424            sink.list();
425            sink.listItem();
426            sink.link(WIKI_TYPECONVERER_URL);
427            sink.text(WIKI_TYPECONVERER_URL);
428            sink.link_();
429            sink.listItem_();
430            sink.list_();
431            sink.paragraph_();
432        }
433    
434        private void tableHeader(Sink sink, Locale locale) {
435            String caption = getBundle(locale).getString(
436                    "report.converters.report.table.caption");
437            String head1 = getBundle(locale).getString(
438                    "report.converters.report.table.head1");
439            String head2 = getBundle(locale).getString(
440                    "report.converters.report.table.head2");
441            String head3 = getBundle(locale).getString(
442                    "report.converters.report.table.head3");
443            String head4 = getBundle(locale).getString(
444                    "report.converters.report.table.head4");
445            String head5 = getBundle(locale).getString(
446                    "report.converters.report.table.head5");
447    
448            sink.tableCaption();
449            sink.text(caption);
450            sink.tableCaption_();
451    
452            sink.tableRow();
453            sink.tableHeaderCell();
454            sink.text(head1);
455            sink.tableHeaderCell_();
456            sink.tableHeaderCell();
457            sink.text(head2);
458            sink.tableHeaderCell_();
459            sink.tableHeaderCell();
460            sink.text(head3);
461            sink.tableHeaderCell_();
462            sink.tableHeaderCell();
463            sink.text(head4);
464            sink.tableHeaderCell_();
465            sink.tableHeaderCell();
466            sink.text(head5);
467            sink.tableHeaderCell_();
468            sink.tableRow();
469        }
470    
471        private void tableRow(Sink sink, String from, String to, String method,
472                Class<?> clazz, String type) {
473    
474            sink.tableRow();
475            sink.tableCell();
476            sink.text(from);
477            sink.tableCell_();
478            sink.tableCell();
479            sink.text(to);
480            sink.tableCell_();
481            sink.tableCell();
482            sink.text(method);
483            sink.tableCell_();
484            sink.tableCell();
485            sink.link(clazz.getName());
486            sink.text(clazz.getSimpleName());
487            sink.link_();
488            sink.tableCell_();
489            sink.tableCell();
490            sink.text(converterType(type));
491            sink.tableCell_();
492            sink.tableRow();
493        }
494    
495        private void endReport(Sink sink) {
496            sink.section1_();
497    
498            sink.body_();
499            sink.flush();
500            sink.close();
501        }
502    }