View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License. 
18   */
19  package org.apache.creadur.whisker.fromxml;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.util.ArrayList;
24  import java.util.Calendar;
25  import java.util.Collection;
26  import java.util.Collections;
27  import java.util.HashMap;
28  import java.util.HashSet;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.SortedSet;
32  import java.util.TreeSet;
33  
34  import org.apache.commons.lang3.StringUtils;
35  import org.apache.creadur.whisker.model.ByOrganisation;
36  import org.apache.creadur.whisker.model.License;
37  import org.apache.creadur.whisker.model.Organisation;
38  import org.apache.creadur.whisker.model.Resource;
39  import org.apache.creadur.whisker.model.WithLicense;
40  import org.apache.creadur.whisker.model.WithinDirectory;
41  import org.apache.creadur.whisker.model.Descriptor;
42  import org.jdom.Document;
43  import org.jdom.Element;
44  import org.jdom.JDOMException;
45  import org.jdom.input.SAXBuilder;
46  
47  /**
48   * Builds a model from xml using JDOM.
49   */
50  public class JDomBuilder {
51      
52      private static final String COPYRIGHT_NOTICE_NAME = "copyright-notice";
53      /**
54       * 
55       */
56      private static final String LICENSE_ELEMENT_NAME = "license";
57      /**
58       * 
59       */
60      private static final String PRIMARY_LICENSE_NAME = "primary-license";
61      /** Names the element representing an organisation */
62      private static final String ORGANISATION_ELEMENT_NAME = "organisation";
63      /** Names the element representing a resource */
64      private static final String RESOURCE_ELEMENT_NAME = "resource";
65  
66      /**
67       * Builds a resource.
68       * @param element not null
69       * @return built resource, not null
70       * @throws UnexpectedElementException when element is not named 'resource'
71       */
72      public Resource resource(Element element) throws UnexpectedElementException {
73          if (RESOURCE_ELEMENT_NAME.equals(element.getName())) {
74              return new Resource(StringUtils.trim(element.getAttributeValue("name")), 
75                      StringUtils.trim(element.getAttributeValue("notice")),
76                      StringUtils.trim(element.getAttributeValue("source")));
77          } else {
78              throw unexpectedElementException(element, RESOURCE_ELEMENT_NAME);
79          }
80      }
81  
82      /**
83       * Builds a suitable exception when the element name is unexpected.
84       * @param element, not null
85       * @param expectedElement, not null
86       * @return a suitable exception, not null
87       */
88      private UnexpectedElementException unexpectedElementException(Element element,
89              final String expectedElement) {
90          return new UnexpectedElementException(expectedElement, element.getName());
91      }
92  
93      /**
94       * Builds an organisation model from xml.
95       * @param element, not null
96       * @return {@link Organisation} not null
97       * @throws UnexpectedElementException when element is not named 'organisation'
98       */
99      public Organisation organisation(Element element) throws UnexpectedElementException {
100         if (ORGANISATION_ELEMENT_NAME.equals(element.getName())) {
101             return new Organisation(
102                     element.getAttributeValue("id"), 
103                     element.getAttributeValue("name"), 
104                     element.getAttributeValue("url"));
105         } else {
106             throw unexpectedElementException(element, ORGANISATION_ELEMENT_NAME);
107         }
108     }
109 
110     /**
111      * @param element
112      * @return
113      */
114     @SuppressWarnings("unchecked")
115     public Collection<Resource> collectResources(Element element) {
116         final Collection<Resource> resources = new TreeSet<Resource>();
117         for (Element resourceElement: (List<Element>) element.getChildren("resource")) {
118             resources.add(new JDomBuilder().resource(resourceElement));
119         }
120         return Collections.unmodifiableCollection(resources);
121     }
122 
123     /**
124      * Finds the organisation linked by ID from the given element.
125      * @param element modelled ByOrganisation, not null
126      * @param organisationsById organisations identified, not null
127      * @throws MissingIDException when the linked organisation is not found in the given map
128      */
129     public Organisation organisation(final Element element,
130             final Map<String, Organisation> organisationsById) throws MissingIDException {
131         final String id = element.getAttributeValue("id");
132         if (organisationsById.containsKey(id)) {
133             return organisationsById.get(id);
134         } else {
135             throw new MissingIDException(ORGANISATION_ELEMENT_NAME, element.getName(), id);
136         }
137     }
138     
139     /**
140      * Builds a by-organisation model from xml.
141      * @param element not null
142      * @param organisation not null
143      * @return not null
144      */
145     public ByOrganisation byOrganisation(final Element element, final Organisation organisation) {
146         return new ByOrganisation(organisation, collectResources(element));
147     }
148 
149     /**
150      * Builds a by-organisation model from xml.
151      * @param byOrganisation not null
152      * @param organisationsById not null 
153      * @return not null
154      * @throws MissingIDException when the linked organisation is not found in the given map
155      */
156     public ByOrganisation byOrganisation(final Element byOrganisation, 
157             final Map<String, Organisation> organisationsById) throws MissingIDException  {
158         return byOrganisation(byOrganisation, organisation(byOrganisation, organisationsById));
159     }
160 
161     /**
162      * Collects by-organisation children.
163      * @param parent not null
164      * @param map not null
165      * @return unmodifiable set sort by natural order, not null
166      */
167     @SuppressWarnings("unchecked")
168     public SortedSet<ByOrganisation> collectByOrganisations(final Element parent, 
169             final Map<String, Organisation> map) {
170         final SortedSet<ByOrganisation> results = new TreeSet<ByOrganisation>();
171         if (parent != null) {
172             for (final Element byOrgElement: (List<Element>) parent.getChildren("by-organisation")) {
173                 results.add(byOrganisation(byOrgElement, map));
174             }
175         }
176         return Collections.unmodifiableSortedSet(results);
177     }
178 
179     /**
180      * Builds a license model from xml.
181      * @param element not null
182      * @return not null
183      */
184     public License license(Element element) {
185         final Element text = element.getChild("text");
186         return new License("yes".equalsIgnoreCase(element.getAttributeValue("requires-source")), 
187                 text == null ? "" : text.getText(), 
188                 expectedParameters(element), 
189                 element.getAttributeValue("id"), 
190                 element.getAttributeValue("url"),
191                 element.getAttributeValue("name"));
192     }
193     
194     @SuppressWarnings("unchecked")
195     private Collection<String> expectedParameters(final Element element) {
196         final Collection<String> results = new HashSet<String>();
197         final Element templateElement = element.getChild("template");
198         if (templateElement != null) {
199             for (Element parameterNameElement: (List<Element>) templateElement.getChildren("parameter-name")) {
200                 results.add(parameterNameElement.getTextTrim());
201             }
202         }
203         return results;
204     }
205 
206     /**
207      * Finds the license with an id matching that referenced by the element.
208      * @param element not null
209      * @param licenses not null
210      * @return not null
211      * @throws MissingIDException when referenced license isn't found in the collection
212      */
213     public License license(final Element element, final Map<String, License> licenses) throws MissingIDException {
214         final String id = element.getAttributeValue("id");
215         if (licenses.containsKey(id)) {
216             return licenses.get(id);
217         } else {
218             throw new MissingIDException(LICENSE_ELEMENT_NAME, element.getName(), id);
219         }
220     }
221 
222     /**
223      * Builds a with-license model from xml.
224      * @param element not null
225      * @param licenses not null
226      * @param organisations not null
227      * @return
228      * @throws MissingIDException when referenced license isn't found in the collection
229      */
230     public WithLicense withLicense(Element element,
231             Map<String, License> licenses,
232             Map<String, Organisation> organisations) throws MissingIDException  {
233         return new WithLicense(license(element, licenses), copyrightNotice(element), 
234                 parameters(element), collectByOrganisations(element, organisations));
235     }
236     
237     /**
238      * Extracts copyright notice content from with-license.
239      * @param element not null
240      * @return not null
241      */
242     private String copyrightNotice(final Element element) {
243         final String result;
244         final Element copyrightNoticeElement = element.getChild(COPYRIGHT_NOTICE_NAME);
245         if (copyrightNoticeElement == null) {
246             result = null;
247         } else {
248             result = copyrightNoticeElement.getTextTrim();
249         }
250         return result;
251     }
252 
253     /**
254      * Builds a list of parameter values by name.
255      * @param element not null
256      * @return parameter values indexed by value, not null
257      * @throws DuplicateElementException when two parameters shared the same name
258      */
259     @SuppressWarnings("unchecked")
260     public Map<String, String> parameters(Element element) throws DuplicateElementException {
261         final Map<String, String> results = new HashMap<String, String>();
262         final Element licenseParametersElement = element.getChild("license-parameters");
263         if (licenseParametersElement != null) {
264             for (Element parameterElement: (List<Element>) licenseParametersElement.getChildren("parameter")) {
265                 final String name = parameterElement.getChild("name").getTextTrim();
266                 if (results.containsKey(name)) {
267                     throw new DuplicateElementException("Duplicate parameter '" + name + "'");
268                 }
269                 results.put(name, 
270                         parameterElement.getChild("value").getTextTrim());
271             }   
272         }
273         return results;
274     }
275 
276     /**
277      * Collects child with-licenses.
278      * @param licenses not null
279      * @param organisations not null
280      * @param parent not null
281      * @return not null, possibly empty
282      */
283     @SuppressWarnings("unchecked")
284     public Collection<WithLicense> withLicenses(Map<String, License> licenses,
285             Map<String, Organisation> organisations, Element parent) {
286         final List<WithLicense> results = new ArrayList<WithLicense>();
287         for (Element withLicenseElement: (List<Element>) parent.getChildren("with-license")) {
288             results.add(new JDomBuilder().withLicense(withLicenseElement, licenses, organisations));
289         }
290         Collections.sort(results);
291         return results;
292     }
293 
294     /**
295      * Collects child organisations of public domain.
296      * @param organisations not null
297      * @param parent not null
298      * @return not null, possibly null
299      */
300     public Collection<ByOrganisation> publicDomain(
301             final Map<String, Organisation> organisations, final Element parent) {
302         return new JDomBuilder().collectByOrganisations(parent.getChild("public-domain"), organisations);
303     }
304 
305     /**
306      * Builds a within directory model from XML.
307      * @param element not null
308      * @param licenses not null
309      * @param organisations not null
310      * @return not null
311      */
312     public WithinDirectory withinDirectory(Element element,
313             Map<String, License> licenses,
314             Map<String, Organisation> organisations) {
315         return new WithinDirectory(element.getAttributeValue("dir"), 
316                 withLicenses(licenses, organisations, element), publicDomain(organisations, element));
317     }
318 
319     /**
320      * Collects organisation definitions within document.
321      * @param document, not null
322      * @return organisations indexed by id, not null possibly empty
323      */
324     public Map<String, Organisation> mapOrganisations(Document document) {
325         final Map<String, Organisation> organisationsById = new HashMap<String, Organisation>();
326         
327         final Element childOrganisations = document.getRootElement().getChild("organisations");
328         if (childOrganisations != null) {
329             @SuppressWarnings("unchecked")
330             final List<Element> organisations = (List<Element>) childOrganisations.getChildren("organisation");
331             for (final Element element: organisations) {
332                 new JDomBuilder().organisation(element).storeIn(organisationsById);
333             }
334         }
335         return Collections.unmodifiableMap(organisationsById);
336     }
337 
338     /**
339      * Collects license definitions within document.
340      * @param document, not null
341      * @return licenses, indexed by id, not null, possibly empty
342      */
343     public Map<String, License> mapLicenses(Document document) {
344         final Map<String, License> results = new HashMap<String, License>();
345         final Element licensesChild = document.getRootElement().getChild("licenses");
346         if (licensesChild != null) {
347             @SuppressWarnings("unchecked")
348             final List<Element> children = (List<Element>) licensesChild.getChildren();
349             for (final Element element: children) {
350                 new JDomBuilder().license(element).storeIn(results);
351             }
352         }
353         return Collections.unmodifiableMap(results);
354 
355     }
356 
357     /**
358      * Finds the primary license for the given document from the given licenses.
359      * @param document not null
360      * @param licenses not null
361      * @return not null
362      */
363     public License primaryLicense(Document document,
364             Map<String, License> licenses) {
365         final String idAttributeValue = getPrimaryLicenseElement(document).getAttributeValue("id");
366         final License results = licenses.get(idAttributeValue);
367         if (results == null) {
368             throw new MissingIDException(LICENSE_ELEMENT_NAME, PRIMARY_LICENSE_NAME, idAttributeValue);
369         }
370         return results;
371     }
372 
373     /**
374      * Gets the element representing the primary license.
375      * @param document not null
376      * @return not null
377      */
378     private Element getPrimaryLicenseElement(final Document document) {
379         return document.getRootElement().getChild(PRIMARY_LICENSE_NAME);
380     }
381 
382     /**
383      * Gets the additional primary copyright notice 
384      * from the document.
385      * @param document not null
386      * @return optional primary copyright notice, possibly null
387      */
388     public String primaryCopyrightNotice(final Document document) {
389         final String result;
390         final Element copyrightElement = 
391                 getPrimaryLicenseElement(document).getChild(COPYRIGHT_NOTICE_NAME);
392         if (copyrightElement == null) {
393             result = null;
394         } else {
395             result = copyrightElement.getTextTrim();
396         }
397         return result;
398     }
399 
400     
401     /**
402      * Collects notices in the given documents.
403      * @param document, not null
404      * @return notices indexed by id, immutable, not null, possibly empty
405      */
406     public Map<String, String> mapNotices(Document document) {
407         final Map<String, String> results = new HashMap<String, String>();
408         final Element noticesElement = document.getRootElement().getChild("notices");
409         if (noticesElement != null){
410             @SuppressWarnings("unchecked")
411             final List<Element> children = (List<Element>) noticesElement.getChildren();
412             for (final Element element: children) {
413                 results.put(element.getAttributeValue("id"), element.getTextTrim());
414             }
415         }
416         return Collections.unmodifiableMap(results);
417 
418     }
419 
420     /**
421      * Retrieves the text of the primary notice.
422      * @param document, not null
423      * @return the text of the primary notice, 
424      * or null when there is no primary notice
425      */
426     public String primaryNotice(Document document) {
427         final String result;
428         final Element primaryNoticeElement = document.getRootElement().getChild("primary-notice");
429         if (primaryNoticeElement == null) {
430             result = null;
431         } else {
432             result = primaryNoticeElement.getText()
433                 .replace("${year}", Integer.toString(Calendar.getInstance().get(Calendar.YEAR)));
434         }
435         return result;
436     }
437 
438     /**
439      * Retrieves the ID of the primary organisation.
440      * @param document, not null
441      * @return the id of the primary organisation when set,
442      * otherwise null
443      */
444     public String primaryOrganisationId(final Document document) {
445         final String result;
446         final Element primaryOrganisationElement = document.getRootElement().getChild("primary-organisation");
447         if (primaryOrganisationElement == null) {
448             result = null;
449         } else {
450             result = primaryOrganisationElement.getAttributeValue("id");
451         }
452         return result;
453     }    
454 
455     private WithinDirectory directory(final Element element, final Map<String, License> licenses,
456             final Map<String, Organisation> organisations) {
457         return new JDomBuilder().withinDirectory(element, licenses, organisations);
458     }
459 
460     /**
461      * Collects contents of the document.
462      * @param document not null
463      * @return not null, possibly empty
464      * @throws DuplicateElementException when dir names are not unique
465      */
466     @SuppressWarnings("PMD.EmptyIfStmt")
467     public Collection<WithinDirectory> collectContents(final Document document, final Map<String, License> licenses,
468             final Map<String, Organisation> organisations) throws DuplicateElementException {
469         final Collection<WithinDirectory> results = new TreeSet<WithinDirectory>();
470         @SuppressWarnings("unchecked")
471         final List<Element> children = document.getRootElement().getChildren("within");
472         for (Element element: children) {
473             if (results.add(directory(element, licenses, organisations))) {
474                 // fine
475             } else {
476                 throw new DuplicateElementException("Duplicate parameter '" + element.getAttribute("dir").getValue() + "'");
477             }
478         }
479         return results;
480     }
481     
482     /**
483      * Builds work from the given document.
484      * @param document not null
485      * @return not null
486      */
487     public Descriptor build(final Document document) {
488         final Map<String, Organisation> organisations = mapOrganisations(document);
489         final Map<String, License> licenses = mapLicenses(document);
490         final Map<String, String> notices = mapNotices(document);
491         final License primaryLicense = primaryLicense(document, licenses);
492         final String primaryCopyrightNotice = primaryCopyrightNotice(document);
493         final String primaryNotice = primaryNotice(document);
494         final String primaryOrganisationId = primaryOrganisationId(document);
495         final Collection<WithinDirectory> contents = collectContents(document, licenses, organisations); 
496         return new Descriptor(primaryLicense, primaryCopyrightNotice,
497                 primaryOrganisationId, primaryNotice, 
498                 licenses, notices, organisations, contents);
499     }
500     
501     public Descriptor build(final InputStream xmlStream) throws JDOMException, IOException {
502         return build(new SAXBuilder().build(xmlStream));
503     }
504 }