/* * Data Hub Service (DHuS) - For Space data distribution. * Copyright (C) 2013,2014,2015 GAEL Systems * * This file is part of DHuS software sources. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package fr.gael.dhus.util; import java.io.StringWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Map.Entry; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; /** * A Metalink v4 builder.<br> * It relies on the Standard Java API v.6 and higher.<br> * It supports the whole Metalink v4 specifications except the * extensions elements. * <p> * Every method returns its instance for method chaining. * <p> * Names of fields in this class are the same as the names of elements in * the produced XML document. * <p> * <b>You MUST add at least one file and each file MUST contains * at least one url.</b> * <p> * See the <a href="http://tools.ietf.org/html/rfc5854"> * RFC for Metalink v4 (RFC5854)</a> * and the <a href="http://www.metalinker.org/">Official website</a>. * <p> * <b>Example:</b> * <pre> * SimpleDateFormat sdf = new SimpleDateFormat( * MetalinkBuilder.DATE_TIME_FORMAT); * sdf.setTimeZone(TimeZone.getTimeZone("UTC")); * * MetalinkBuilder mb = new MetalinkBuilder(); * mb.setGenerator("DHuS/3.8.1") * .setOrigin("http://dhus.gael.fr:8080/", true) * .setPublished(sdf.format(new Date())) * .addFile( * "S1A_S3_SLC__1ASV_20140507T105003_20140507T105033_000490_0005F3_A57E.zip") * .setHash("MD5", "324sdf65468G4EQ34H68QS4FGH3QS847H") * .setPublisher("European Space Agency (ESA)", "http://esa.int/") * .setSize(5000000) * .addUrl("http://dhus.gael.fr:8080/odata/v1/ * Products('148539fa-18b6-11e4-a1de-b2227cce2b54')/$value", null, 0) * Document meta4 = mb.build(); * * Transformer transformer = TransformerFactory.newInstance().newTransformer(); * transformer.setOutputProperty(OutputKeys.INDENT, "yes"); * transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", * "2"); * transformer.transform(new DOMSource(meta4), new StreamResult(System.out)); * </pre> */ public class MetalinkBuilder { /** The content type of the produced document. */ public static final String CONTENT_TYPE = "application/metalink4+xml"; /** The file extension for a Metalink v4 document is ".meta4" */ public static final String FILE_EXTENSION = ".meta4"; /** The "date_time" date format defined in the * <a href="http://tools.ietf.org/html/rfc3339">RFC3339</a>. */ public static final String DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; /** Single static instance of DocBuilder because ServiceLoader is slow. */ private static final DocumentBuilder DOC_BUILDER; /** Single static instance of Transformer because ServiceLoader is slow. */ private static final Transformer TRANSFORMER; private String generator = null; /** Contains an IRI and has a boolean attribute. */ private BasicElement origin = null; /** Contains a date_time */ private String published = null; /** Contains a date_time */ private String updated = null; private final ArrayList<MetalinkFileBuilder> files = new ArrayList<MetalinkFileBuilder> (); static { try { DOC_BUILDER = DocumentBuilderFactory.newInstance().newDocumentBuilder(); TRANSFORMER = TransformerFactory.newInstance().newTransformer(); TRANSFORMER.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); } catch (ParserConfigurationException ex) { throw new Error("Cannot instanciate DocBuilder with default configuration", ex); } catch (TransformerConfigurationException ex) { throw new Error("Cannot instanciate Transformer with default configuration", ex); } } /** * Builds the XML document. * @return A non-null instance of Document containing the whole XML tree. * @throws IllegalStateException if there is not file and/or no url in a * file. */ public Document build () { // Validating if (files.size () == 0) throw new IllegalStateException ("MetalinkBuilder has no file."); for (MetalinkFileBuilder fb: files) if (fb.url.size () == 0) throw new IllegalStateException ("MetalinkFileBuilder has no url."); Document doc = DOC_BUILDER.newDocument(); // Root of the XML document Element rootElement = doc.createElement("metalink"); rootElement.setAttribute ("xmlns", "urn:ietf:params:xml:ns:metalink"); doc.appendChild (rootElement); // Children elements if (generator != null && !generator.isEmpty ()) appendTextElement(doc, rootElement, "generator", generator); if (origin != null) origin.build (doc, rootElement); if (published != null) appendTextElement(doc, rootElement, "published", published); if (updated != null) appendTextElement(doc, rootElement, "updated", updated); for (MetalinkFileBuilder fb: files) fb.build (doc, rootElement); return doc; } /** * Builds and stringify this metalink document. * @param indent {@code true} if the returned XML doc must be indented. * @return an XML doc as string. * @throws TransformerException if the stringification failed. */ public String buildToString(boolean indent) throws TransformerException { StringWriter sw = new StringWriter(); TRANSFORMER.setOutputProperty(OutputKeys.INDENT, (indent)? "yes": "no"); Document doc = build(); TRANSFORMER.transform(new DOMSource(doc), new StreamResult(sw)); return sw.toString(); } /** * Utility method to create and append a new element in the XML tree. * Creates a new Element and appends it to the given parent node. * @param doc The XML DOM document. * @param parent The parent node in the XML tree. * @param name The name of the new element. * @param value The text value of the new element. */ private static void appendTextElement (Document doc, Node parent, String name, String value) { Element e = doc.createElement(name); e.appendChild (doc.createTextNode (value)); parent.appendChild (e); } /** * The `generator` element identifies the generating agent name and version * used to generate a Metalink Document. * @param generator "AgentName/AgentVersion" eg: "DHuS/3.8.1" * @return this. */ public MetalinkBuilder setGenerator (String generator) { this.generator = generator; return this; } /** * The `origin` element is an IRI where the Metalink Document was * originally published. * @param origin An Internationalized Resource Identifiers which can * be an URI. * @param dynamic If true, then updated versions of the Metalink can * be found at this IRI. * @return this. */ public MetalinkBuilder setOrigin (String origin, boolean dynamic) { this.origin = new BasicElement ("origin", origin); this.origin.addAttribute ("dynamic", dynamic); return this; } /** * The `published` element is a Date indicating the initial creation or * first availability of the resource. * @param published A date_time, see {@link #DATE_TIME_FORMAT}. * @return this. */ public MetalinkBuilder setPublished (String published) { this.published = published; return this; } /** * The `updated` element is a Date indicating the most recent instant * in time when a Metalink was modified. * @param updated A date_time, see {@link #DATE_TIME_FORMAT}. * @return this. */ public MetalinkBuilder setUpdated (String updated) { this.updated = updated; return this; } /** * Adds a file to this metalink document. * You MUST add at least one file. * @param name The local file name to which the downloaded file * will be written. * @return A new builder for the new file element. * @see MetalinkFileBuilder */ public MetalinkFileBuilder addFile (String name) { MetalinkFileBuilder newFile = new MetalinkFileBuilder(name); files.add(newFile); return newFile; } /** * File element.<br> * You MUST add at least one url.<br> * See the <a href="http://tools.ietf.org/html/rfc5854#section-4.1.2"> * RFC</a>. */ public class MetalinkFileBuilder { /** An attribute of the `file` element. */ private String name = null; private String copyright = null; private String description = null; /** Contains a String and has a `type` attribute. */ private final HashMap<String, String> hash = new HashMap<String, String> (); private String identity = null; /** Constains a String. */ private final ArrayList<String> language = new ArrayList<String> (); /** Constains a IRI. */ private String logo = null; /** Contains an IRI and has two String and one Int attributes. */ private final ArrayList<BasicElement> metaUrls = new ArrayList<BasicElement> (); /** Contains a String. */ private final ArrayList<String> os = new ArrayList<String> (); /** Contains two String attributes. */ private BasicElement publisher = null; /** Contains a String and has a String attribute. */ private BasicElement signature = null; /** Contains a positive Int. */ private long size = -1; /** Contains a String and has one String and one Int attributes. */ private final ArrayList<BasicElement> url = new ArrayList<BasicElement> (); private String version = null; private final HashSet<MetalinkFilePiecesBuilder> pieces = new HashSet<MetalinkFilePiecesBuilder>(); /** @see MetalinkBuilder#addFile(String) */ private MetalinkFileBuilder (String name) { this.name = name; } /** * Creates a new file Element and appends it to root node. * @param doc The XML DOM document. * @param root The root node in the XML tree. */ private void build (Document doc, Node root) { Element file = doc.createElement ("file"); file.setAttribute ("name", name); root.appendChild (file); if (copyright != null && !copyright.isEmpty ()) appendTextElement(doc, file, "copyright", copyright); if (description != null && !description.isEmpty ()) appendTextElement(doc, file, "description", description); for (Entry<String, String> e: hash.entrySet ()) { Element hash = doc.createElement("hash"); hash.setAttribute ("type", e.getKey ()); hash.appendChild (doc.createTextNode (e.getValue ())); file.appendChild (hash); } if (identity != null && !identity.isEmpty ()) appendTextElement(doc, file, "identity", identity); for (String lang: language) appendTextElement(doc, file, "language", lang); if (logo != null && !logo.isEmpty ()) appendTextElement(doc, file, "logo", logo); for (BasicElement be: metaUrls) be.build (doc, file); for (String os: this.os) appendTextElement(doc, file, "os", os); if (publisher != null) publisher.build (doc, file); if (signature != null) signature.build (doc, file); if (size >= 0) appendTextElement(doc, file, "size", String.valueOf (size)); for (BasicElement url: this.url) url.build (doc, file); if (version != null && !version.isEmpty ()) appendTextElement(doc, file, "version", version); for (MetalinkFilePiecesBuilder pieces: this.pieces) pieces.build (doc, file); } /** * The `copyright` element is a Text that conveys the copyright * for this file. * @return this. */ public MetalinkFileBuilder setCopyright (String copyright) { this.copyright = copyright; return this; } /** * The `description` element is a Text that describe this file. * @return this. */ public MetalinkFileBuilder setDescription (String description) { this.description = description; return this; } /** * The `hash` element is a Text that conveys a cryptographic hash * for this file. * @param type The type of hash. eg: "SHA-1", "MD5", "SHA-256". * @param hash The hash for this file. * @return this. */ public MetalinkFileBuilder setHash (String type, String hash) { this.hash.put (type, hash); return this; } /** * The `identity` element is a Text that conveys an identity * for this file. * @param identity eg: "EO Product". * @return this. */ public MetalinkFileBuilder setIdentity (String identity) { this.identity = identity; return this; } /** * The `language` element is a Text that conveys a code for the * language of this file. * The String parameter MUST conform to the * <a href="http://tools.ietf.org/html/rfc5646">RFC5646</a>. * @param language A non-null language code. eg: "en-GB". * @return this. */ public MetalinkFileBuilder addLanguage (String language) { if (language != null && !language.isEmpty ()) this.language.add ( language); return this; } /** * The `logo` element's content is an IRI to an image that provides * visual identification for a file. * @return this. */ public MetalinkFileBuilder setLogo (String logo) { this.logo = logo; return this; } /** * The `metaurl` element contains the IRI of a metadata file * (aka a metainfo file), about a resource to download. * This could be the IRI of a BitTorrent .torrent file, a * Metalink Document, or other type of metadata file. * @param url A non-null URL to the metadata file. * @param mediatype A non-null type for the referenced document. * eg: "torrent". * @param name The name of the file in the referenced document * (can be null). * @param priority A number between 1 and 999999 (inclusive). * Lower values indicate a higher priority. * @return this. */ public MetalinkFileBuilder addMetaUrl (String url, String mediatype, String name, int priority) { BasicElement metaUrl = new BasicElement ("metaurl", url); metaUrl.addAttribute ("mediatype", mediatype); if (name != null) metaUrl.addAttribute ("mediatype", mediatype); if (priority > 1 && priority < 1000000) metaUrl.addAttribute ( "priority", priority); this.metaUrls.add (metaUrl); return this; } /** * The `os` element is a Text that conveys an Operating System that * this file is suitable for. * @param os A non-null IANA "Operating System Name" eg: "WIN32", * "LINUX", "OSX". * @return this. */ public MetalinkFileBuilder addOs (String os) { if (os != null && !os.isEmpty ()) this.os.add (os); return this; } /** * The `publisher` element contains the name of entity that has * published the file described in the * Metalink Document and an IRI for more information. * @param name A non-null name of the publisher. * @param url An URL to the site of the publisher (can be null). * @return this. */ public MetalinkFileBuilder setPublisher (String name, String url) { this.publisher = new BasicElement ("publisher", null); this.publisher.addAttribute ("name", name); if (url != null) this.publisher.addAttribute ("url", url); return this; } /** * The `signature` element is a Text that conveys a digital signature * for this file. * @param signature The signature of this file. * @param mediatype The type of the signature. * eg: "application/pgp-signature". * @return this. */ public MetalinkFileBuilder setSignature (String signature, String mediatype) { this.signature = new BasicElement ("signature", signature); this.signature.addAttribute ("mediatype", mediatype); return this; } /** * The `size` element indicates the length of this file in octets. * @param size A non-negative Integer. * @return this. */ public MetalinkFileBuilder setSize (long size) { this.size = size; return this; } /** * The `url` element contains a file IRI. * @param url An url to this file. * @param location An [ISO3166-1] 2 letters country code, eg: "gb", * "us" (Can be null). * @param priority A number between 1 and 999999 (inclusive). * Lower values indicate a higher priority. * @return this. */ public MetalinkFileBuilder addUrl (String url, String location, int priority) { BasicElement be = new BasicElement ("url", url); if (location != null && location.length () == 2) be.addAttribute ( "location", location); if (priority > 0 && priority < 1000000) be.addAttribute ( "priority", priority); this.url.add (be); return this; } /** * The `version` element is a Text that conveys a version for this file. * @param version eg: "3.8" * @return this. */ public MetalinkFileBuilder setVersion (String version) { this.version = version; return this; } /** * A container for a list of cryptographic hashes of contiguous, * non-overlapping pieces of this file. * @param type The type of hash. eg: "SHA-1", "MD5", "SHA-256". * @param length The length of a piece of the file. * @return A new builder for the new pieces element. * @see MetalinkFilePiecesBuilder */ public MetalinkFilePiecesBuilder setPieces (String type, long length) { MetalinkFilePiecesBuilder pieces = new MetalinkFilePiecesBuilder ( type, length); this.pieces.add (pieces); return pieces; } /** * Pieces element. * A container for a list of cryptographic hashes of contiguous, * non-overlapping pieces of a file. */ public class MetalinkFilePiecesBuilder { private String type; private long length; private final ArrayList<String> hash = new ArrayList<String> (); /** @see MetalinkFileBuilder#setPieces */ private MetalinkFilePiecesBuilder (String type, long length) { if (type.isEmpty () || length < 1) throw new IllegalArgumentException ( "Bad param for MetalinkFilePiecesBuilder"); this.type = type; this.length = length; } /** * Creates a new pieces Element and appends it to a file node. * @param doc The XML DOM document. * @param file A file node in the XML tree. */ private void build (Document doc, Node file) { Element pieces = doc.createElement ("pieces"); pieces.setAttribute ("type", type); pieces.setAttribute ("length", String.valueOf (length)); file.appendChild (pieces); for (String hash: this.hash) appendTextElement (doc, pieces, "hash", hash); } /** * The `hash` element is a Text that conveys a cryptographic hash * for a piece of this file. * Hashes MUST be added in the same order as the corresponding * pieces appear in the file. * @return this. */ public MetalinkFilePiecesBuilder addHash (String hash) { if (hash.isEmpty ()) throw new IllegalArgumentException ("Empty hash not allowed"); this.hash.add (hash); return this; } // Overrides equals and hashCode because instances of this // class are stored in a HashSet. @Override public int hashCode () { return type.hashCode (); } @Override public boolean equals (Object obj) { if (obj == null) return false; if (this == obj) return true; if (getClass () != obj.getClass ()) return false; MetalinkFilePiecesBuilder other = (MetalinkFilePiecesBuilder) obj; if (type == null && other.type != null) return false; return type.equals (other.type); } } } /** * Basic Element * Elements with no child */ private class BasicElement { private final String name; private final ArrayList<String> attributesNames = new ArrayList<String> (); private final ArrayList<Object> attributesValues = new ArrayList<Object> (); private final String value; /** * Creates a new BasicElement. * @param name The name of the element. * @param value The value of the element. * @throws NullPointerException if name is null. * @throws IllegalArgumentException if name is empty. */ BasicElement (String name, String value) { if (name.isEmpty ()) throw new IllegalArgumentException ("Empty name not allowed."); this.name = name; this.value = value; } /** * Add an attribute. * @param name The name of the attribute. * @param value The value of the attribute (its toString method will be * used to write the document). * @throws NullPointerException if name or value is null. * @throws IllegalArgumentException if name is empty. */ void addAttribute(String name, Object value) { if (name.isEmpty ()) throw new IllegalArgumentException ("Empty name not allowed."); if (value == null) throw new NullPointerException ("Null value not allowed."); this.attributesNames.add (name); this.attributesValues.add (value); } /** * Creates a new Element and appends it to the given parent node. * @param doc The XML DOM document. * @param parent The parent node in the XML tree. */ private void build (Document doc, Node parent) { Element e = doc.createElement (name); if (value != null && !value.isEmpty ()) e.appendChild ( doc.createTextNode (value)); for (int i=0; i<attributesNames.size (); i++) e.setAttribute (attributesNames.get (i), attributesValues.get (i).toString ()); parent.appendChild (e); } } }