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 }