View Javadoc

1   /*
2    * Copyright 2007-2013 smartics, Kronseder & Reiner GmbH
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package de.smartics.maven.exceptions.sdoc;
17  
18  import java.io.File;
19  import java.util.HashMap;
20  import java.util.List;
21  import java.util.Map;
22  import java.util.MissingResourceException;
23  import java.util.ResourceBundle;
24  
25  import javax.xml.stream.XMLStreamException;
26  
27  import org.apache.commons.lang.StringUtils;
28  import org.apache.commons.logging.Log;
29  import org.apache.commons.logging.LogFactory;
30  import org.w3c.dom.DOMException;
31  import org.w3c.dom.Document;
32  import org.w3c.dom.Element;
33  import org.w3c.dom.Text;
34  
35  import com.thoughtworks.qdox.model.JavaAnnotatedElement;
36  import com.thoughtworks.qdox.model.JavaClass;
37  import com.thoughtworks.qdox.model.JavaField;
38  
39  import de.smartics.exceptions.core.Code;
40  import de.smartics.exceptions.i18n.message.MessageType;
41  import de.smartics.exceptions.report.data.ExceptionCodeReportItem;
42  import de.smartics.exceptions.report.data.ProjectConfiguration;
43  import de.smartics.exceptions.report.message.PlaceHolderDesc;
44  import de.smartics.exceptions.report.message.PlaceHolderInfo;
45  import de.smartics.exceptions.report.renderer.JavadocRenderer;
46  import de.smartics.maven.exceptions.util.TextCoder;
47  import de.smartics.messages.core.BundleMapper;
48  import de.smartics.xml.DomCopyReader;
49  
50  /**
51   * Creates the content for a projectdoc code doctype instance.
52   */
53  public class SdocCodeBuilder
54  { // NOPMD
55    // ********************************* Fields *********************************
56  
57    // --- constants ------------------------------------------------------------
58  
59    /**
60     * Reference to the logger for this class.
61     */
62    private static final Log LOG = LogFactory.getLog(SdocCodeBuilder.class);
63  
64    /**
65     * The URI of the XML schema instance.
66     * <p>
67     * The value of this constant is {@value}.
68     * </p>
69     */
70    private static final String XML_SCHEMA_INSTANCE =
71        "http://www.w3.org/2001/XMLSchema-instance";
72  
73    /**
74     * The URI of the code doctype.
75     * <p>
76     * The value of this constant is {@value}.
77     * </p>
78     */
79    private static final String CODE_URI =
80        "http://www.smartics.de/project/process/implementation/appcode";
81  
82    // --- members --------------------------------------------------------------
83  
84    /**
85     * The renderer to use for processing Javadoc comments.
86     */
87    private final JavadocRenderer renderer;
88  
89    /**
90     * Helper to encode and decode texts according to the encodings requested by
91     * the project configuration.
92     */
93    private final TextCoder coder;
94  
95    /**
96     * The mapping of a code instance to a bundle.
97     */
98    private final BundleMapper mapper;
99  
100   /**
101    * The resource bundle with labels to render.
102    */
103   private final ResourceBundle bundle;
104 
105   /**
106    * The empty document to write to.
107    */
108   private final Document document;
109 
110   /**
111    * The code representation of the runtime environement.
112    */
113   private final String codeRepresentation;
114 
115   /**
116    * Static information about the code to write to the document.
117    */
118   private final ExceptionCodeReportItem codeContainer;
119 
120   // ****************************** Initializer *******************************
121 
122   // ****************************** Constructors ******************************
123 
124   /**
125    * Default constructor.
126    *
127    * @param renderer the renderer to use for processing Javadoc comments.
128    * @param config the project configuration.
129    * @param document the empty document to write to.
130    * @param codeRepresentation the code representation of the runtime
131    *          environment.
132    * @param codeContainer the value for codeContainer.
133    */
134   SdocCodeBuilder(final JavadocRenderer renderer,
135       final ProjectConfiguration<File> config, final Document document,
136       final String codeRepresentation,
137       final ExceptionCodeReportItem codeContainer)
138   {
139     this.coder = new TextCoder(config);
140     this.renderer = renderer;
141     this.mapper = config.getBundleMapper();
142     this.bundle = config.getBundle();
143     this.document = document;
144     this.codeRepresentation = codeRepresentation;
145     this.codeContainer = codeContainer;
146   }
147 
148   // ****************************** Inner Classes *****************************
149 
150   // ********************************* Methods ********************************
151 
152   // --- init -----------------------------------------------------------------
153 
154   // --- get&set --------------------------------------------------------------
155 
156   // --- business -------------------------------------------------------------
157 
158   /**
159    * Writes the content to the document.
160    */
161   public Document writeDocumentContent()
162   {
163     final JavaField fieldDoc = codeContainer.getJavadoc();
164     final JavaClass classDoc = codeContainer.getTypeJavadoc();
165 
166     final TagInfo tagInfo = new TagInfo(classDoc, fieldDoc);
167 
168     final Element docRoot = createDocRoot();
169     createContentElement("id", codeRepresentation, docRoot);
170     createContentElement("name", tagInfo.getName(), docRoot); // NOPMD
171     createContentElement("type", tagInfo.getCodeType(), docRoot);
172 
173     final String componentId = codeContainer.getCode().getComponentId();
174     createContentElement("component", componentId, docRoot);
175 
176     createImplementationElement(classDoc.getFullyQualifiedName(),
177         fieldDoc.getName(), docRoot);
178     createContentElement("category", tagInfo.getCategory(), docRoot);
179     createContentElement("subcategory", tagInfo.getSubcategory(), docRoot);
180     createContentElement("sort-key", tagInfo.getSortKey(), docRoot);
181     createContentElement("parents", "parent", tagInfo.getParents(), docRoot);
182     createContentElement("tags", "tag", tagInfo.getTags(), docRoot);
183     renderShortDescription(tagInfo, fieldDoc, docRoot);
184     renderJavadocContent("package-description", docRoot,
185         createJavadoc(classDoc));
186     renderJavadocContent("description", docRoot, createJavadoc(fieldDoc)); // NOPMD
187     createContentElement("notes", tagInfo.getNotes(), docRoot);
188 
189     renderMessages(docRoot);
190 
191     return document;
192   }
193 
194   private void renderShortDescription(final TagInfo tagInfo,
195       final JavaField fieldDoc, final Element docRoot) throws DOMException
196   {
197     String text = tagInfo.getShortDescription();
198     if (StringUtils.isBlank(text))
199     {
200       final String comment = fieldDoc.getComment();
201       if (comment != null)
202       {
203         final int index = comment.indexOf('.'); // FIXME: Too crude, dot my be
204                                                 // escaped.
205         if (index != -1 && index < comment.length() - 2)
206         {
207           final String firstSentence = comment.substring(0, index + 1);
208           text = firstSentence;
209         }
210         else
211         {
212           text = comment;
213         }
214       }
215     }
216 
217     if (StringUtils.isNotBlank(text))
218     {
219       renderJavadocContent("short-description", docRoot, text);
220     }
221   }
222 
223   private void createImplementationElement(final String implementingClassName,
224       final String implementingFieldName, final Element docRoot)
225   {
226     final Element implementation = document.createElement("implementation");
227     docRoot.appendChild(implementation);
228     createContentElement("class", implementingClassName, implementation);
229     createContentElement("field", implementingFieldName, implementation);
230   }
231 
232   private void createContentElement(final String groupGi, final String gi,
233       final List<String> contents, final Element docRoot)
234   {
235     if (!contents.isEmpty())
236     {
237       final Element group = document.createElement(groupGi);
238       docRoot.appendChild(group);
239       for (final String content : contents)
240       {
241         createContentElement(gi, content, group);
242       }
243     }
244   }
245 
246   private void renderJavadocContent(final String gi,
247       final Element parentElement, final String content) throws DOMException
248   {
249     if (StringUtils.isNotBlank(content))
250     {
251       final Element element = document.createElement(gi);
252       parentElement.appendChild(element);
253       final DomCopyReader copy = new DomCopyReader(document, element);
254       try
255       {
256         copy.copy(content);
257       }
258       catch (final XMLStreamException e)
259       {
260         if (LOG.isWarnEnabled())
261         {
262           LOG.warn("Cannot parse Javdoc content as XML. Using it verbatim: "
263                    + content);
264         }
265         createContentElement("description", content, parentElement);
266       }
267     }
268   }
269 
270   private Element createContentElement(final String gi, final String content,
271       final Element parent)
272   {
273     if (content != null)
274     {
275       final String encoded = coder.run(content);
276       return createPlainContentElement(gi, encoded, parent);
277     }
278     return null;
279   }
280 
281   private Element createPlainContentElement(final String gi,
282       final String content, final Element parent)
283   {
284     if (content != null)
285     {
286       final Element element = document.createElement(gi);
287       final Text text = document.createTextNode(content);
288       element.appendChild(text);
289       parent.appendChild(element);
290       return element;
291     }
292     return null;
293   }
294 
295   private String createJavadoc(final JavaAnnotatedElement doc)
296   {
297     final String javadoc = this.renderer.render(doc);
298     return StringUtils.isNotBlank(javadoc) ? coder.run(javadoc) : "&#160;";
299   }
300 
301   private Element createDocRoot() throws DOMException
302   {
303     final Element docRoot = document.createElement("appcode");
304     docRoot.setAttribute("xmlns:xsi", XML_SCHEMA_INSTANCE);
305     docRoot.setAttribute("xmlns", CODE_URI);
306     docRoot.setAttribute("xsi:schemaLocation", CODE_URI + ' ' + CODE_URI);
307     document.appendChild(docRoot);
308     return docRoot;
309   }
310 
311   private void renderMessages(final Element parent)
312   {
313     if (mapper != null)
314     {
315       final Code code = codeContainer.getCode();
316       final Class<?> codeClass = code.getClass();
317       try
318       {
319         final ResourceBundle messageBundle =
320             mapper.getBundle(codeClass, this.bundle.getLocale());
321         if (messageBundle != null)
322         {
323           addMessages(parent, codeContainer, messageBundle);
324         }
325       }
326       catch (final MissingResourceException e)
327       {
328         if (LOG.isWarnEnabled())
329         {
330           LOG.warn("Cannot load resource bundle for code class '" + codeClass
331                    + "'.", e);
332         }
333       }
334     }
335   }
336 
337   private void addMessages(final Element parent,
338       final ExceptionCodeReportItem codeContainer, final ResourceBundle bundle)
339     throws DOMException
340   {
341     final Element messages = document.createElement("messages");
342     addMessages(codeContainer, bundle, messages);
343     if (messages.hasChildNodes())
344     {
345       parent.appendChild(messages);
346     }
347   }
348 
349   private void addMessages(final ExceptionCodeReportItem codeContainer,
350       final ResourceBundle bundle, final Element messages) throws DOMException
351   {
352     for (MessageType type : MessageType.values())
353     {
354       final String keyPrefix = codeContainer.getCode().getCode();
355       final String key = type.createKey(keyPrefix);
356       addText(codeContainer, bundle, messages, type, key);
357     }
358   }
359 
360   private void addText(final ExceptionCodeReportItem codeContainer,
361       final ResourceBundle bundle, final Element messages,
362       final MessageType messageType, final String key) throws DOMException
363   {
364     try
365     {
366       final String text = bundle.getString(key);
367       if (StringUtils.isNotBlank(text))
368       {
369         final String encoded = text; // coder.run(text);
370         final String typeId = getTypeId(messageType);
371         final Element message = document.createElement("message");
372         createContentElement("name", renderTypeId(typeId), message);
373         final Element textElement =
374             createPlainContentElement("text", encoded, message);
375         final Map<String, String> placeHolderMap =
376             addPlaceHolders(messageType, codeContainer, message, bundle,
377                 encoded);
378         if (!placeHolderMap.isEmpty())
379         {
380           replaceText(placeHolderMap, textElement);
381         }
382         messages.appendChild(message);
383       }
384     }
385     catch (final MissingResourceException e)
386     {
387       // ok, skip
388     }
389   }
390 
391   private String renderTypeId(final String typeId)
392   {
393     try
394     {
395       final String message = bundle.getString("label." + typeId);
396       return message;
397     }
398     catch (final Exception e)
399     {
400       if (LOG.isDebugEnabled())
401       {
402         LOG.debug("No translation for '" + typeId
403                   + "' found in resource bundle.");
404       }
405       return typeId;
406     }
407   }
408 
409   private void replaceText(final Map<String, String> placeHolderMap,
410       final Element textElement)
411   {
412     final Text textNode = (Text) textElement.getFirstChild();
413     String text = textNode.getTextContent();
414     for (Map.Entry<String, String> entry : placeHolderMap.entrySet())
415     {
416       final String key = entry.getKey();
417       if (isNumeric(key))
418       {
419         final String value = entry.getValue();
420         if(text.contains(key))
421         {
422           text = StringUtils.replace(text, key, '{' + value + '}');
423         }
424         else
425         {
426           final String altKey = StringUtils.chop(key) + ',';
427           text = StringUtils.replace(text, altKey, '{' + value + ',');
428         }
429       }
430     }
431     text = StringUtils.replace(text, "''", "'");
432     textNode.setTextContent(text);
433   }
434 
435   private boolean isNumeric(final String key)
436   {
437     final int length = key.length();
438     boolean foundDigit = false;
439     for (int i = 1; i < length; i++)
440     {
441       final char c = key.charAt(i);
442       if (!Character.isDigit(c))
443       {
444         break;
445       }
446       foundDigit = Character.isDigit(c);
447     }
448 
449     return foundDigit;
450   }
451 
452   private Map<String, String> addPlaceHolders(final MessageType messageType,
453       final ExceptionCodeReportItem codeContainer, final Element parent,
454       final ResourceBundle bundle, final String messageText)
455     throws DOMException
456   {
457     final Element placeholders = document.createElement("placeholders");
458     final Map<String, String> placeHolderMap =
459         addPlaceholder(messageType, codeContainer.getPlaceHolderInfo(), bundle,
460             placeholders, messageText);
461     if (placeholders.hasChildNodes())
462     {
463       parent.appendChild(placeholders);
464     }
465     return placeHolderMap;
466   }
467 
468   private Map<String, String> addPlaceholder(final MessageType messageType,
469       final PlaceHolderInfo placeHolderInfo, final ResourceBundle bundle,
470       final Element placeholders, final String messageText)
471   {
472     final Map<String, String> placeHolderMap = new HashMap<String, String>();
473 
474     for (PlaceHolderDesc desc : placeHolderInfo
475         .getPlaceHolderDescs(messageType))
476     {
477       final Element placeholder = document.createElement("placeholder");
478       final String index = desc.getPlaceHolderIndex();
479       final String placeHolderString = '{' + index + '}';
480       if (messageText.contains(placeHolderString)
481           || messageText.contains('{' + index + ','))
482       {
483         final String path = desc.getOgnlPath();
484         final String name =
485             desc.getParamName()
486                 + (path != null ? ':' + desc.getOgnlPath() : "");
487         final String description = desc.getDescription();
488 
489         if (StringUtils.isNumeric(index))
490         {
491           createContentElement("name", name, placeholder);
492         }
493         else
494         {
495           createContentElement("name", index, placeholder);
496         }
497         createContentElement("description", description, placeholder);
498         placeholders.appendChild(placeholder);
499 
500         placeHolderMap.put(placeHolderString, name);
501       }
502     }
503 
504     return placeHolderMap;
505   }
506 
507   private String getTypeId(final MessageType type)
508   {
509     final String id = type.getMessageKeySuffix();
510     if (StringUtils.EMPTY.equals(id))
511     {
512       return "summary";
513     }
514     else
515     {
516       return id.substring(1);
517     }
518   }
519 
520   // --- object basics --------------------------------------------------------
521 }