All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.taverna.robundle.manifest.Manifest Maven / Gradle / Ivy

There is a newer version: 0.15.1-incubating
Show newest version
package org.apache.taverna.robundle.manifest;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import static com.fasterxml.jackson.databind.SerializationFeature.FAIL_ON_EMPTY_BEANS;
import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT;
import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_EMPTY_JSON_ARRAYS;
import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_NULL_MAP_VALUES;
import static java.nio.file.FileVisitResult.CONTINUE;
import static java.nio.file.FileVisitResult.SKIP_SUBTREE;
import static java.nio.file.Files.createDirectories;
import static java.nio.file.Files.getLastModifiedTime;
import static java.nio.file.Files.isDirectory;
import static java.nio.file.Files.newBufferedWriter;
import static java.nio.file.Files.walkFileTree;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;
import static java.nio.file.attribute.FileTime.fromMillis;
import static org.apache.taverna.robundle.Bundles.uriToBundlePath;

import java.io.IOException;
import java.io.Writer;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

import org.apache.taverna.robundle.Bundle;
import org.apache.taverna.robundle.manifest.combine.CombineManifest;
import org.apache.taverna.robundle.manifest.odf.ODFManifest;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.ObjectMapper;

@JsonPropertyOrder(value = { "@context", "id", "manifest", "createdOn",
		"createdBy", "createdOn", "authoredOn", "authoredBy", "history",
		"aggregates", "annotations", "@graph" })
public class Manifest {
	public abstract class FileTimeMixin {
		@Override
		@JsonValue
		public abstract String toString();
	}

	public abstract class PathMixin {
		@Override
		@JsonValue
		public abstract String toString();
	}

	private static Logger logger = Logger.getLogger(Manifest.class
			.getCanonicalName());

	private static final String MANIFEST_JSON = "manifest.json";

	private static final String META_INF = "/META-INF";

	private static final String MIMETYPE = "/mimetype";
	private static final String RO = "/.ro";
	private static URI ROOT = URI.create("/");

	public static FileTime now() {
		return fromMillis(new GregorianCalendar().getTimeInMillis());
	}

	protected static Path withSlash(Path dir) {
		if (dir == null)
			return null;
		if (isDirectory(dir)) {
			Path fname = dir.getFileName();
			if (fname == null)
				return dir;
			String fnameStr = fname.toString();
			if (fnameStr.endsWith("/"))
				return dir;
			return dir.resolveSibling(fnameStr + "/");
		}
		return dir;
	}

	private Map aggregates = new LinkedHashMap<>();
	private List annotations = new ArrayList<>();
	private List authoredBy = new ArrayList<>();
	private FileTime authoredOn;
	private Bundle bundle;
	private Agent createdBy = null;
	private FileTime createdOn = now();
	private List graph;
	private List history = new ArrayList<>();
	private URI id = URI.create("/");
	private List manifest = new ArrayList<>();

	public Manifest(Bundle bundle) {
		this.bundle = bundle;
	}

	public List getAggregates() {
		return new ArrayList<>(aggregates.values());
	}

	public PathMetadata getAggregation(Path file) {
		URI fileUri = file.toUri();
		return getAggregation(fileUri);
	}

	public PathMetadata getAggregation(URI uri) {
		uri = relativeToBundleRoot(uri);
		PathMetadata metadata = aggregates.get(uri);
		if (metadata == null) {
			metadata = new PathMetadata();
			if (!uri.isAbsolute() && uri.getFragment() == null) {
				Path path = uriToBundlePath(bundle, uri);
				metadata.setFile(path);
				metadata.setMediatype(guessMediaType(path));
			} else {
				metadata.setUri(uri);
			}
			aggregates.put(uri, metadata);
		}
		return metadata;
	}

	public List getAnnotations() {
		return annotations;
	}

	public List getAuthoredBy() {
		return authoredBy;
	}

	public FileTime getAuthoredOn() {
		return authoredOn;
	}

	@JsonIgnore
	public URI getBaseURI() {
		return getBundle().getRoot().toUri();
	}

	@JsonIgnore
	public Bundle getBundle() {
		return bundle;
	}

	@JsonProperty(value = "@context")
	public List getContext() {
		ArrayList context = new ArrayList<>();
		// HashMap map = new HashMap<>();
		// map.put("@base", getBaseURI());
		// context.add(map);
		context.add(URI.create("https://w3id.org/bundle/context"));
		return context;
	}

	public Agent getCreatedBy() {
		return createdBy;
	}

	public FileTime getCreatedOn() {
		return createdOn;
	}

	public List getGraph() {
		return graph;
	}

	public List getHistory() {
		return history;
	}

	public URI getId() {
		return id;
	}

	public List getManifest() {
		return manifest;
	}

	/**
	 * Guess media type based on extension
	 * 
	 * @see http://wf4ever.github.io/ro/bundle/#media-types
	 * 
	 * @param file
	 *            A Path to a file
	 * @return media-type, e.g. application/xml or
	 *         text/plain; charset="utf-8"
	 */
	public String guessMediaType(Path file) {
		if (file.getFileName() == null)
			return null;
		String filename = file.getFileName().toString()
				.toLowerCase(Locale.ENGLISH);
		if (filename.endsWith(".txt"))
			return "text/plain; charset=\"utf-8\"";
		if (filename.endsWith(".ttl"))
			return "text/turtle; charset=\"utf-8\"";
		if (filename.endsWith(".rdf") || filename.endsWith(".owl"))
			return "application/rdf+xml";
		if (filename.endsWith(".json"))
			return "application/json";
		if (filename.endsWith(".jsonld"))
			return "application/ld+json";
		if (filename.endsWith(".xml"))
			return "application/xml";

		// A few extra, common ones

		if (filename.endsWith(".png"))
			return "image/png";
		if (filename.endsWith(".svg"))
			return "image/svg+xml";
		if (filename.endsWith(".jpg") || filename.endsWith(".jpeg"))
			return "image/jpeg";
		if (filename.endsWith(".pdf"))
			return "application/pdf";
		return "application/octet-stream";
	}

	public void populateFromBundle() throws IOException {
		final Set potentiallyEmptyFolders = new LinkedHashSet<>();

		final Set existingAggregationsToPrune = new HashSet<>(
				aggregates.keySet());

		walkFileTree(bundle.getRoot(), new SimpleFileVisitor() {
			@SuppressWarnings("deprecation")
			@Override
			public FileVisitResult postVisitDirectory(Path dir, IOException exc)
					throws IOException {
				super.postVisitDirectory(dir, exc);
				if (potentiallyEmptyFolders.remove(dir)) {
					URI uri = relativeToBundleRoot(dir.toUri());
					existingAggregationsToPrune.remove(uri);
					PathMetadata metadata = aggregates.get(uri);
					if (metadata == null) {
						metadata = new PathMetadata();
						aggregates.put(uri, metadata);
					}
					metadata.setFile(withSlash(dir));
					metadata.setFolder(withSlash(dir.getParent()));
					metadata.setProxy();
					metadata.setCreatedOn(getLastModifiedTime(dir));
					potentiallyEmptyFolders.remove(withSlash(dir.getParent()));
					return CONTINUE;
				}
				return CONTINUE;
			}

			@Override
			public FileVisitResult preVisitDirectory(Path dir,
					BasicFileAttributes attrs) throws IOException {
				if (dir.startsWith(RO) || dir.startsWith(META_INF))
					return SKIP_SUBTREE;
				potentiallyEmptyFolders.add(withSlash(dir));
				potentiallyEmptyFolders.remove(withSlash(dir.getParent()));
				return CONTINUE;
			}

			@SuppressWarnings("deprecation")
			@Override
			public FileVisitResult visitFile(Path file,
					BasicFileAttributes attrs) throws IOException {
				potentiallyEmptyFolders.remove(withSlash(file.getParent()));
				if (file.startsWith(MIMETYPE))
					return CONTINUE;
				if (manifest.contains(file))
					// Don't aggregate the manifests
					return CONTINUE;

				// super.visitFile(file, attrs);
				URI uri = relativeToBundleRoot(file.toUri());
				existingAggregationsToPrune.remove(uri);
				PathMetadata metadata = aggregates.get(uri);
				if (metadata == null) {
					metadata = new PathMetadata();
					aggregates.put(uri, metadata);
				}
				metadata.setFile(file);
				if (metadata.getMediatype() == null)
					// Don't override if already set
					metadata.setMediatype(guessMediaType(file));
				metadata.setFolder(withSlash(file.getParent()));
				metadata.setProxy();
				metadata.setCreatedOn(getLastModifiedTime(file));
				potentiallyEmptyFolders.remove(file.getParent());
				return CONTINUE;
			}
		});
		for (URI preExisting : existingAggregationsToPrune) {
			PathMetadata meta = aggregates.get(preExisting);
			if (meta.getFile() != null)
				/*
				 * Don't remove 'virtual' resources, only aggregations that went
				 * to files
				 */
				aggregates.remove(preExisting);
		}
	}

	public URI relativeToBundleRoot(URI uri) {
		uri = ROOT.resolve(bundle.getRoot().toUri().relativize(uri));
		return uri;
	}

	@SuppressWarnings("deprecation")
	public void setAggregates(List aggregates) {
		this.aggregates.clear();

		for (PathMetadata meta : aggregates) {
			URI uri = null;
			if (meta.getFile() != null) {
				uri = relativeToBundleRoot(meta.getFile().toUri());
			} else if (meta.getUri() != null) {
				uri = relativeToBundleRoot(meta.getUri());
			} else {
				uri = relativeToBundleRoot(meta.getProxy());
			}
			if (uri == null) {
				logger.warning("Unknown URI for aggregation " + meta);
				continue;
			}
			this.aggregates.put(uri, meta);
		}
	}

	public void setAnnotations(List annotations) {
		this.annotations = annotations;
	}

	public void setAuthoredBy(List authoredBy) {
		if (authoredBy == null)
			throw new NullPointerException("authoredBy can't be null");
		this.authoredBy = authoredBy;
	}

	public void setAuthoredOn(FileTime authoredOn) {
		this.authoredOn = authoredOn;
	}

	public void setBundle(Bundle bundle) {
		this.bundle = bundle;
	}

	public void setCreatedBy(Agent createdBy) {
		this.createdBy = createdBy;
	}

	public void setCreatedOn(FileTime createdOn) {
		this.createdOn = createdOn;
	}

	public void setGraph(List graph) {
		this.graph = graph;
	}

	public void setHistory(List history) {
		if (history == null)
			throw new NullPointerException("history can't be null");
		this.history = history;
	}

	public void setId(URI id) {
		this.id = id;
	}

	public void setManifest(List manifest) {
		this.manifest = manifest;
	}

	public void writeAsCombineManifest() throws IOException {
		new CombineManifest(this).createManifestXML();
	}

	/**
	 * Write as an RO Bundle JSON-LD manifest
	 * 
	 * @return The path of the written manifest (e.g. ".ro/manifest.json")
	 * @throws IOException
	 */
	public Path writeAsJsonLD() throws IOException {
		Path jsonld = bundle.getFileSystem().getPath(RO, MANIFEST_JSON);
		createDirectories(jsonld.getParent());
		// Files.createFile(jsonld);
		if (!getManifest().contains(jsonld))
			getManifest().add(0, jsonld);
		ObjectMapper om = new ObjectMapper();
		om.addMixInAnnotations(Path.class, PathMixin.class);
		om.addMixInAnnotations(FileTime.class, FileTimeMixin.class);
		om.enable(INDENT_OUTPUT);
		om.disable(WRITE_EMPTY_JSON_ARRAYS);
		om.disable(FAIL_ON_EMPTY_BEANS);
		om.disable(WRITE_NULL_MAP_VALUES);

		om.setSerializationInclusion(Include.NON_NULL);
		try (Writer w = newBufferedWriter(jsonld, Charset.forName("UTF-8"),
				WRITE, TRUNCATE_EXISTING, CREATE)) {
			om.writeValue(w, this);
		}
		return jsonld;
	}

	/**
	 * Write as a ODF manifest.xml
	 * 
	 * @see http
	 *      ://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part3.
	 *      html#__RefHeading__752807_826425813
	 * @return The path of the written manifest (e.g. "META-INF/manifest.xml")
	 * @throws IOException
	 */
	public Path writeAsODFManifest() throws IOException {
		return new ODFManifest(this).createManifestXML();
	}
}