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 org.apache.commons.lang3.StringUtils;
22  import org.apache.creadur.whisker.model.*;
23  import org.jdom2.Document;
24  import org.jdom2.Element;
25  import org.jdom2.JDOMException;
26  import org.jdom2.input.SAXBuilder;
27  
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.util.*;
31  
32  /**
33   * Builds a model from xml using JDOM.
34   */
35  public class JDomBuilder {
36  
37  	private static final String COPYRIGHT_NOTICE_NAME = "copyright-notice";
38  	/**
39       * 
40       */
41  	private static final String LICENSE_ELEMENT_NAME = "license";
42  	/**
43       * 
44       */
45  	private static final String PRIMARY_LICENSE_NAME = "primary-license";
46  	/** Names the element representing an organisation */
47  	private static final String ORGANISATION_ELEMENT_NAME = "organisation";
48  	/** Names the element representing a resource */
49  	private static final String RESOURCE_ELEMENT_NAME = "resource";
50  
51  	/**
52  	 * Builds a resource.
53  	 * 
54  	 * @param element
55  	 *            not null
56  	 * @return built resource, not null
57  	 * @throws UnexpectedElementException
58  	 *             when element is not named 'resource'
59  	 */
60  	public Resource resource(Element element) throws UnexpectedElementException {
61  		if (RESOURCE_ELEMENT_NAME.equals(element.getName())) {
62  			return new Resource(StringUtils.trim(element
63  					.getAttributeValue("name")), StringUtils.trim(element
64  					.getAttributeValue("notice")), StringUtils.trim(element
65  					.getAttributeValue("source")));
66  		} else {
67  			throw unexpectedElementException(element, RESOURCE_ELEMENT_NAME);
68  		}
69  	}
70  
71  	/**
72  	 * Builds a suitable exception when the element name is unexpected.
73  	 * 
74  	 * @param element
75  	 *            , not null
76  	 * @param expectedElement
77  	 *            , not null
78  	 * @return a suitable exception, not null
79  	 */
80  	private UnexpectedElementException unexpectedElementException(
81  			Element element, final String expectedElement) {
82  		return new UnexpectedElementException(expectedElement,
83  				element.getName());
84  	}
85  
86  	/**
87  	 * Builds an organisation model from xml.
88  	 * 
89  	 * @param element
90  	 *            , not null
91  	 * @return {@link Organisation} not null
92  	 * @throws UnexpectedElementException
93  	 *             when element is not named 'organisation'
94  	 */
95  	public Organisation organisation(Element element)
96  			throws UnexpectedElementException {
97  		if (ORGANISATION_ELEMENT_NAME.equals(element.getName())) {
98  			return new Organisation(element.getAttributeValue("id"),
99  					element.getAttributeValue("name"),
100 					element.getAttributeValue("url"));
101 		} else {
102 			throw unexpectedElementException(element, ORGANISATION_ELEMENT_NAME);
103 		}
104 	}
105 
106 	/**
107 	 * @param element
108 	 *            JDOM element to collect.
109 	 * @return collected resources.
110 	 */
111 	@SuppressWarnings("unchecked")
112 	public Collection<Resource> collectResources(Element element) {
113 		final Collection<Resource> resources = new TreeSet<Resource>();
114 		for (Element resourceElement : (List<Element>) element
115 				.getChildren("resource")) {
116 			resources.add(new JDomBuilder().resource(resourceElement));
117 		}
118 		return Collections.unmodifiableCollection(resources);
119 	}
120 
121 	/**
122 	 * @return Finds the organisation linked by ID from the given element.
123 	 * @param element
124 	 *            modelled ByOrganisation, not null
125 	 * @param organisationsById
126 	 *            organisations identified, not null
127 	 * @throws MissingIDException
128 	 *             when the linked organisation is not found in the given map
129 	 */
130 	public Organisation organisation(final Element element,
131 			final Map<String, Organisation> organisationsById)
132 			throws MissingIDException {
133 		final String id = element.getAttributeValue("id");
134 		if (organisationsById.containsKey(id)) {
135 			return organisationsById.get(id);
136 		} else {
137 			throw new MissingIDException(ORGANISATION_ELEMENT_NAME,
138 					element.getName(), id);
139 		}
140 	}
141 
142 	/**
143 	 * Builds a by-organisation model from xml.
144 	 * 
145 	 * @param element
146 	 *            not null
147 	 * @param organisation
148 	 *            not null
149 	 * @return not null
150 	 */
151 	public ByOrganisation byOrganisation(final Element element,
152 			final Organisation organisation) {
153 		return new ByOrganisation(organisation, collectResources(element));
154 	}
155 
156 	/**
157 	 * Builds a by-organisation model from xml.
158 	 * 
159 	 * @param byOrganisation
160 	 *            not null
161 	 * @param organisationsById
162 	 *            not null
163 	 * @return not null
164 	 * @throws MissingIDException
165 	 *             when the linked organisation is not found in the given map
166 	 */
167 	public ByOrganisation byOrganisation(final Element byOrganisation,
168 			final Map<String, Organisation> organisationsById)
169 			throws MissingIDException {
170 		return byOrganisation(byOrganisation,
171 				organisation(byOrganisation, organisationsById));
172 	}
173 
174 	/**
175 	 * Collects by-organisation children.
176 	 * 
177 	 * @param parent
178 	 *            not null
179 	 * @param map
180 	 *            not null
181 	 * @return unmodifiable set sort by natural order, not null
182 	 */
183 	@SuppressWarnings("unchecked")
184 	public SortedSet<ByOrganisation> collectByOrganisations(
185 			final Element parent, final Map<String, Organisation> map) {
186 		final SortedSet<ByOrganisation> results = new TreeSet<ByOrganisation>();
187 		if (parent != null) {
188 			for (final Element byOrgElement : (List<Element>) parent
189 					.getChildren("by-organisation")) {
190 				results.add(byOrganisation(byOrgElement, map));
191 			}
192 		}
193 		return Collections.unmodifiableSortedSet(results);
194 	}
195 
196 	/**
197 	 * Builds a license model from xml.
198 	 * 
199 	 * @param element
200 	 *            not null
201 	 * @return not null
202 	 */
203 	public License license(Element element) {
204 		final Element text = element.getChild("text");
205 		return new License("yes".equalsIgnoreCase(element
206 				.getAttributeValue("requires-source")), text == null ? ""
207 				: text.getText(), expectedParameters(element),
208 				element.getAttributeValue("id"),
209 				element.getAttributeValue("url"),
210 				element.getAttributeValue("name"));
211 	}
212 
213 	@SuppressWarnings("unchecked")
214 	private Collection<String> expectedParameters(final Element element) {
215 		final Collection<String> results = new HashSet<String>();
216 		final Element templateElement = element.getChild("template");
217 		if (templateElement != null) {
218 			for (Element parameterNameElement : (List<Element>) templateElement
219 					.getChildren("parameter-name")) {
220 				results.add(parameterNameElement.getTextTrim());
221 			}
222 		}
223 		return results;
224 	}
225 
226 	/**
227 	 * Finds the license with an id matching that referenced by the element.
228 	 * 
229 	 * @param element
230 	 *            not null
231 	 * @param licenses
232 	 *            not null
233 	 * @return not null
234 	 * @throws MissingIDException
235 	 *             when referenced license isn't found in the collection
236 	 */
237 	public License license(final Element element,
238 			final Map<String, License> licenses) throws MissingIDException {
239 		final String id = element.getAttributeValue("id");
240 		if (licenses.containsKey(id)) {
241 			return licenses.get(id);
242 		} else {
243 			throw new MissingIDException(LICENSE_ELEMENT_NAME,
244 					element.getName(), id);
245 		}
246 	}
247 
248 	/**
249 	 * @return Builds a with-license model from xml.
250 	 * @param element
251 	 *            not null
252 	 * @param licenses
253 	 *            not null
254 	 * @param organisations
255 	 *            not null
256 	 * 
257 	 * @throws MissingIDException
258 	 *             when referenced license isn't found in the collection
259 	 */
260 	public WithLicense withLicense(Element element,
261 			Map<String, License> licenses,
262 			Map<String, Organisation> organisations) throws MissingIDException {
263 		return new WithLicense(license(element, licenses),
264 				copyrightNotice(element), parameters(element),
265 				collectByOrganisations(element, organisations));
266 	}
267 
268 	/**
269 	 * Extracts copyright notice content from with-license.
270 	 * 
271 	 * @param element
272 	 *            not null
273 	 * @return not null
274 	 */
275 	private String copyrightNotice(final Element element) {
276 		final String result;
277 		final Element copyrightNoticeElement = element
278 				.getChild(COPYRIGHT_NOTICE_NAME);
279 		if (copyrightNoticeElement == null) {
280 			result = null;
281 		} else {
282 			result = copyrightNoticeElement.getTextTrim();
283 		}
284 		return result;
285 	}
286 
287 	/**
288 	 * Builds a list of parameter values by name.
289 	 * 
290 	 * @param element
291 	 *            not null
292 	 * @return parameter values indexed by value, not null
293 	 * @throws DuplicateElementException
294 	 *             when two parameters shared the same name
295 	 */
296 	@SuppressWarnings("unchecked")
297 	public Map<String, String> parameters(Element element)
298 			throws DuplicateElementException {
299 		final Map<String, String> results = new HashMap<String, String>();
300 		final Element licenseParametersElement = element
301 				.getChild("license-parameters");
302 		if (licenseParametersElement != null) {
303 			for (Element parameterElement : (List<Element>) licenseParametersElement
304 					.getChildren("parameter")) {
305 				final String name = parameterElement.getChild("name")
306 						.getTextTrim();
307 				if (results.containsKey(name)) {
308 					throw new DuplicateElementException("Duplicate parameter '"
309 							+ name + "'");
310 				}
311 				results.put(name, parameterElement.getChild("value")
312 						.getTextTrim());
313 			}
314 		}
315 		return results;
316 	}
317 
318 	/**
319 	 * Collects child with-licenses.
320 	 * 
321 	 * @param licenses
322 	 *            not null
323 	 * @param organisations
324 	 *            not null
325 	 * @param parent
326 	 *            not null
327 	 * @return not null, possibly empty
328 	 */
329 	@SuppressWarnings("unchecked")
330 	public Collection<WithLicense> withLicenses(Map<String, License> licenses,
331 			Map<String, Organisation> organisations, Element parent) {
332 		final List<WithLicense> results = new ArrayList<WithLicense>();
333 		for (Element withLicenseElement : (List<Element>) parent
334 				.getChildren("with-license")) {
335 			results.add(new JDomBuilder().withLicense(withLicenseElement,
336 					licenses, organisations));
337 		}
338 		Collections.sort(results);
339 		return results;
340 	}
341 
342 	/**
343 	 * Collects child organisations of public domain.
344 	 * 
345 	 * @param organisations
346 	 *            not null
347 	 * @param parent
348 	 *            not null
349 	 * @return not null, possibly null
350 	 */
351 	public Collection<ByOrganisation> publicDomain(
352 			final Map<String, Organisation> organisations, final Element parent) {
353 		return new JDomBuilder().collectByOrganisations(
354 				parent.getChild("public-domain"), organisations);
355 	}
356 
357 	/**
358 	 * Builds a within directory model from XML.
359 	 * 
360 	 * @param element
361 	 *            not null
362 	 * @param licenses
363 	 *            not null
364 	 * @param organisations
365 	 *            not null
366 	 * @return not null
367 	 */
368 	public WithinDirectory withinDirectory(Element element,
369 			Map<String, License> licenses,
370 			Map<String, Organisation> organisations) {
371 		return new WithinDirectory(element.getAttributeValue("dir"),
372 				withLicenses(licenses, organisations, element), publicDomain(
373 						organisations, element));
374 	}
375 
376 	/**
377 	 * Collects organisation definitions within document.
378 	 * 
379 	 * @param document
380 	 *            , not null
381 	 * @return organisations indexed by id, not null possibly empty
382 	 */
383 	public Map<String, Organisation> mapOrganisations(Document document) {
384 		final Map<String, Organisation> organisationsById = new HashMap<String, Organisation>();
385 
386 		final Element childOrganisations = document.getRootElement().getChild(
387 				"organisations");
388 		if (childOrganisations != null) {
389 			@SuppressWarnings("unchecked")
390 			final List<Element> organisations = (List<Element>) childOrganisations
391 					.getChildren("organisation");
392 			for (final Element element : organisations) {
393 				new JDomBuilder().organisation(element).storeIn(
394 						organisationsById);
395 			}
396 		}
397 		return Collections.unmodifiableMap(organisationsById);
398 	}
399 
400 	/**
401 	 * Collects license definitions within document.
402 	 * 
403 	 * @param document
404 	 *            , not null
405 	 * @return licenses, indexed by id, not null, possibly empty
406 	 */
407 	public Map<String, License> mapLicenses(Document document) {
408 		final Map<String, License> results = new HashMap<String, License>();
409 		final Element licensesChild = document.getRootElement().getChild(
410 				"licenses");
411 		if (licensesChild != null) {
412 			@SuppressWarnings("unchecked")
413 			final List<Element> children = (List<Element>) licensesChild
414 					.getChildren();
415 			for (final Element element : children) {
416 				new JDomBuilder().license(element).storeIn(results);
417 			}
418 		}
419 		return Collections.unmodifiableMap(results);
420 
421 	}
422 
423 	/**
424 	 * Finds the primary license for the given document from the given licenses.
425 	 * 
426 	 * @param document
427 	 *            not null
428 	 * @param licenses
429 	 *            not null
430 	 * @return not null
431 	 */
432 	public License primaryLicense(Document document,
433 			Map<String, License> licenses) {
434 		final String idAttributeValue = getPrimaryLicenseElement(document)
435 				.getAttributeValue("id");
436 		final License results = licenses.get(idAttributeValue);
437 		if (results == null) {
438 			throw new MissingIDException(LICENSE_ELEMENT_NAME,
439 					PRIMARY_LICENSE_NAME, idAttributeValue);
440 		}
441 		return results;
442 	}
443 
444 	/**
445 	 * Gets the element representing the primary license.
446 	 * 
447 	 * @param document
448 	 *            not null
449 	 * @return not null
450 	 */
451 	private Element getPrimaryLicenseElement(final Document document) {
452 		return document.getRootElement().getChild(PRIMARY_LICENSE_NAME);
453 	}
454 
455 	/**
456 	 * Gets the additional primary copyright notice from the document.
457 	 * 
458 	 * @param document
459 	 *            not null
460 	 * @return optional primary copyright notice, possibly null
461 	 */
462 	public String primaryCopyrightNotice(final Document document) {
463 		final String result;
464 		final Element copyrightElement = getPrimaryLicenseElement(document)
465 				.getChild(COPYRIGHT_NOTICE_NAME);
466 		if (copyrightElement == null) {
467 			result = null;
468 		} else {
469 			result = copyrightElement.getTextTrim();
470 		}
471 		return result;
472 	}
473 
474 	/**
475 	 * Collects notices in the given documents.
476 	 * 
477 	 * @param document
478 	 *            , not null
479 	 * @return notices indexed by id, immutable, not null, possibly empty
480 	 */
481 	public Map<String, String> mapNotices(Document document) {
482 		final Map<String, String> results = new HashMap<String, String>();
483 		final Element noticesElement = document.getRootElement().getChild(
484 				"notices");
485 		if (noticesElement != null) {
486 			@SuppressWarnings("unchecked")
487 			final List<Element> children = (List<Element>) noticesElement
488 					.getChildren();
489 			for (final Element element : children) {
490 				results.put(element.getAttributeValue("id"),
491 						element.getTextTrim());
492 			}
493 		}
494 		return Collections.unmodifiableMap(results);
495 
496 	}
497 
498 	/**
499 	 * Retrieves the text of the primary notice.
500 	 * 
501 	 * @param document
502 	 *            , not null
503 	 * @return the text of the primary notice, or null when there is no primary
504 	 *         notice
505 	 */
506 	public String primaryNotice(Document document) {
507 		final String result;
508 		final Element primaryNoticeElement = document.getRootElement()
509 				.getChild("primary-notice");
510 		if (primaryNoticeElement == null) {
511 			result = null;
512 		} else {
513 			result = primaryNoticeElement.getText()
514 					.replace(
515 							"${year}",
516 							Integer.toString(Calendar.getInstance().get(
517 									Calendar.YEAR)));
518 		}
519 		return result;
520 	}
521 
522 	/**
523 	 * Retrieves the ID of the primary organisation.
524 	 * 
525 	 * @param document
526 	 *            , not null
527 	 * @return the id of the primary organisation when set, otherwise null
528 	 */
529 	public String primaryOrganisationId(final Document document) {
530 		final String result;
531 		final Element primaryOrganisationElement = document.getRootElement()
532 				.getChild("primary-organisation");
533 		if (primaryOrganisationElement == null) {
534 			result = null;
535 		} else {
536 			result = primaryOrganisationElement.getAttributeValue("id");
537 		}
538 		return result;
539 	}
540 
541 	private WithinDirectory directory(final Element element,
542 			final Map<String, License> licenses,
543 			final Map<String, Organisation> organisations) {
544 		return new JDomBuilder().withinDirectory(element, licenses,
545 				organisations);
546 	}
547 
548 	/**
549 	 * Collects contents of the document.
550 	 * 
551 	 * @param document
552 	 *            not null
553 	 * @param licenses
554 	 *            not null
555 	 * @param organisations
556 	 *            not null
557 	 * @return not null, possibly empty
558 	 * @throws DuplicateElementException
559 	 *             when directory names are not unique
560 	 */
561 	public Collection<WithinDirectory> collectContents(final Document document,
562 			final Map<String, License> licenses,
563 			final Map<String, Organisation> organisations)
564 			throws DuplicateElementException {
565 		final Collection<WithinDirectory> results = new TreeSet<WithinDirectory>();
566 		@SuppressWarnings("unchecked")
567 		final List<Element> children = document.getRootElement().getChildren(
568 				"within");
569 		for (Element element : children) {
570 			boolean addedSuccessfully = results.add(directory(element,
571 					licenses, organisations));
572 
573 			if (!addedSuccessfully) {
574 				throw new DuplicateElementException("Duplicate parameter '"
575 						+ element.getAttribute("dir").getValue() + "'");
576 			}
577 		}
578 		return results;
579 	}
580 
581 	/**
582 	 * Builds work from the given document.
583 	 * 
584 	 * @param document
585 	 *            not null
586 	 * @return not null
587 	 */
588 	public Descriptor build(final Document document) {
589 		final Map<String, Organisation> organisations = mapOrganisations(document);
590 		final Map<String, License> licenses = mapLicenses(document);
591 		final Map<String, String> notices = mapNotices(document);
592 		final License primaryLicense = primaryLicense(document, licenses);
593 		final String primaryCopyrightNotice = primaryCopyrightNotice(document);
594 		final String primaryNotice = primaryNotice(document);
595 		final String primaryOrganisationId = primaryOrganisationId(document);
596 		final Collection<WithinDirectory> contents = collectContents(document,
597 				licenses, organisations);
598 		return new Descriptor(primaryLicense, primaryCopyrightNotice,
599 				primaryOrganisationId, primaryNotice, licenses, notices,
600 				organisations, contents);
601 	}
602 
603 	public Descriptor build(final InputStream xmlStream) throws JDOMException,
604 			IOException {
605 		return build(new SAXBuilder().build(xmlStream));
606 	}
607 }