| package org.apache.taverna.scufl2.ucfpackage; |
| /* |
| * |
| * 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 java.io.File.createTempFile; |
| import static java.util.logging.Level.INFO; |
| |
| import java.io.BufferedInputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.io.PipedInputStream; |
| import java.io.PipedOutputStream; |
| import java.net.URI; |
| import java.nio.charset.Charset; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.logging.Logger; |
| |
| import javax.xml.bind.JAXBContext; |
| import javax.xml.bind.JAXBElement; |
| import javax.xml.bind.JAXBException; |
| import javax.xml.bind.Marshaller; |
| import javax.xml.bind.Unmarshaller; |
| |
| import org.apache.taverna.robundle.manifest.odf.ODFJaxb; |
| import org.apache.taverna.scufl2.ucfpackage.impl.odfdom.pkg.OdfPackage; |
| import org.apache.taverna.scufl2.ucfpackage.impl.odfdom.pkg.manifest.OdfFileEntry; |
| import org.apache.taverna.robundle.xml.odf.container.Container; |
| import org.apache.taverna.robundle.xml.odf.container.Container.RootFiles; |
| import org.apache.taverna.robundle.xml.odf.container.ObjectFactory; |
| import org.apache.taverna.robundle.xml.odf.container.RootFile; |
| import org.w3c.dom.Document; |
| |
| |
| public class UCFPackage extends ODFJaxb implements Cloneable { |
| private static Logger logger = Logger.getLogger(UCFPackage.class.getName()); |
| private static final String CONTAINER_XML = "META-INF/container.xml"; |
| private static final Charset UTF_8 = Charset.forName("utf-8"); |
| public static final String MIME_BINARY = "application/octet-stream"; |
| public static final String MIME_TEXT_PLAIN = "text/plain"; |
| public static final String MIME_TEXT_XML = "text/xml"; |
| public static final String MIME_RDF = "application/rdf+xml"; |
| public static final String MIME_EPUB = "application/epub+zip"; |
| public static final String MIME_WORKFLOW_BUNDLE = "application/vnd.taverna.workflow-bundle"; |
| public static final String MIME_DATA_BUNDLE = "application/vnd.taverna.data-bundle"; |
| public static final String MIME_WORKFLOW_RUN_BUNDLE = "application/vnd.taverna.workflow-run-bundle"; |
| public static final String MIME_SERVICE_BUNDLE = "application/vnd.taverna.service-bundle"; |
| |
| private static Charset ASCII = Charset.forName("ascii"); |
| private OdfPackage odfPackage; |
| private static JAXBContext jaxbContext; |
| private JAXBElement<Container> containerXml; |
| private boolean createdContainerXml = false; |
| private static ObjectFactory containerFactory = new ObjectFactory(); |
| |
| public UCFPackage() throws IOException { |
| try { |
| odfPackage = OdfPackage.create(); |
| parseContainerXML(); |
| } catch (IOException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new IOException("Could not create empty UCF Package", e); |
| } |
| // odfPackage.setMediaType(MIME_EPUB); |
| } |
| |
| public UCFPackage(File containerFile) throws IOException { |
| open(containerFile); |
| } |
| |
| protected void open(File containerFile) throws IOException { |
| try (BufferedInputStream stream = new BufferedInputStream( |
| new FileInputStream(containerFile))) { |
| open(stream); |
| } |
| } |
| |
| public UCFPackage(InputStream inputStream) throws IOException { |
| open(inputStream); |
| } |
| |
| protected void open(InputStream inputStream) throws IOException { |
| try { |
| odfPackage = OdfPackage.loadPackage(inputStream); |
| parseContainerXML(); |
| } catch (IOException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new IOException( |
| "Could not load UCF Package from input stream", e); |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| protected void parseContainerXML() throws IOException { |
| createdContainerXml = false; |
| InputStream containerStream = getResourceAsInputStream(CONTAINER_XML); |
| if (containerStream == null) { |
| // Make an empty containerXml |
| Container container = containerFactory.createContainer(); |
| containerXml = containerFactory.createContainer(container); |
| createdContainerXml = true; |
| return; |
| } |
| try { |
| Unmarshaller unMarshaller = createUnMarshaller(); |
| containerXml = (JAXBElement<Container>) unMarshaller |
| .unmarshal(containerStream); |
| } catch (JAXBException e) { |
| throw new IOException("Could not parse " + CONTAINER_XML, e); |
| } |
| } |
| |
| public String getPackageMediaType() { |
| return odfPackage.getMediaType(); |
| } |
| |
| public void setPackageMediaType(String mediaType) { |
| if (mediaType == null || !mediaType.contains("/")) |
| throw new IllegalArgumentException("Invalid media type " |
| + mediaType); |
| if (!ASCII.newEncoder().canEncode(mediaType)) |
| throw new IllegalArgumentException("Media type must be ASCII: " |
| + mediaType); |
| odfPackage.setMediaType(mediaType); |
| } |
| |
| public void save(File packageFile) throws IOException { |
| File tempFile = createTempFile("." + packageFile.getName(), ".tmp", |
| packageFile.getCanonicalFile().getParentFile()); |
| prepareAndSave(tempFile); |
| boolean renamed = tempFile.renameTo(packageFile); |
| if (!renamed && packageFile.exists() && tempFile.exists()) { |
| // Could happen on Windows |
| if (!packageFile.delete()) |
| // Could have been permission problem |
| throw new IOException("Could not delete existing " |
| + packageFile); |
| renamed = tempFile.renameTo(packageFile); |
| } |
| if (!renamed) |
| throw new IOException("Could not rename temp file " + tempFile |
| + " to " + packageFile); |
| } |
| |
| protected void prepareAndSave(File tempFile) throws IOException { |
| if (getPackageMediaType() == null) |
| throw new IllegalStateException("Package media type must be set"); |
| |
| // Write using temp file, and do rename in the end |
| |
| try { |
| prepareContainerXML(); |
| odfPackage.save(tempFile); |
| } catch (IOException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new IOException("Could not save bundle to " + tempFile, e); |
| } finally { |
| odfPackage.close(); |
| } |
| |
| try { |
| open(tempFile); |
| } catch (Exception e) { |
| throw new IOException("Could not reload package from " + tempFile, |
| e); |
| } |
| } |
| |
| protected void prepareContainerXML() throws IOException { |
| if (containerXml == null || createdContainerXml |
| && containerXml.getValue().getRootFilesOrAny() == null) |
| return; |
| |
| /* Check if we should prune <rootFiles> */ |
| Iterator<Object> iterator = containerXml.getValue().getRootFilesOrAny() |
| .iterator(); |
| boolean foundAlready = false; |
| while (iterator.hasNext()) { |
| Object anyOrRoot = iterator.next(); |
| if (!(anyOrRoot instanceof JAXBElement)) |
| continue; |
| @SuppressWarnings("rawtypes") |
| JAXBElement elem = (JAXBElement) anyOrRoot; |
| if (!elem.getDeclaredType().equals(RootFiles.class)) |
| continue; |
| RootFiles rootFiles = (RootFiles) elem.getValue(); |
| if (foundAlready |
| || (rootFiles.getOtherAttributes().isEmpty() && rootFiles |
| .getAnyOrRootFile().isEmpty())) { |
| // Delete it! |
| System.err.println("Deleting unneccessary <rootFiles>"); |
| iterator.remove(); |
| } |
| foundAlready = true; |
| } |
| |
| Marshaller marshaller; |
| try { |
| marshaller = createMarshaller(); |
| // XMLStreamWriter xmlStreamWriter = XMLOutputFactory |
| // .newInstance().createXMLStreamWriter(outStream); |
| // xmlStreamWriter.setDefaultNamespace(containerElem.getName() |
| // .getNamespaceURI()); |
| // |
| // xmlStreamWriter.setPrefix("dsig", |
| // "http://www.w3.org/2000/09/xmldsig#"); |
| // xmlStreamWriter.setPrefix("xmlenc", |
| // "http://www.w3.org/2001/04/xmlenc#"); |
| try (OutputStream outStream = odfPackage |
| .insertOutputStream(CONTAINER_XML)) { |
| // FIXME: Set namespace prefixes and default namespace |
| marshaller.setProperty("jaxb.formatted.output", true); |
| // TODO: Ensure using default namespace |
| marshaller.marshal(containerXml, outStream); |
| } |
| } catch (IOException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new IOException("Could not parse " + CONTAINER_XML, e); |
| } |
| } |
| |
| public void addResource(String stringValue, String path, String mediaType) |
| throws IOException { |
| try { |
| odfPackage.insert(stringValue.getBytes(UTF_8), path, mediaType); |
| } catch (IOException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new IOException("Could not add " + path, e); |
| } |
| parseContainerXML(); |
| } |
| |
| public void addResource(byte[] bytesValue, String path, String mediaType) |
| throws IOException { |
| try { |
| odfPackage.insert(bytesValue, path, mediaType); |
| } catch (IOException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new IOException("Could not add " + path, e); |
| } |
| if (path.equals(CONTAINER_XML)) |
| parseContainerXML(); |
| } |
| |
| public void addResource(Document document, String path, String mediaType) |
| throws IOException { |
| try { |
| odfPackage.insert(document, path, mediaType); |
| } catch (IOException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new IOException("Could not add " + path, e); |
| } |
| if (path.equals(CONTAINER_XML)) |
| parseContainerXML(); |
| } |
| |
| public void addResource(InputStream inputStream, String path, |
| String mediaType) throws IOException { |
| try { |
| odfPackage.insert(inputStream, path, mediaType); |
| } catch (IOException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new IOException("Could not add " + path, e); |
| } |
| if (path.equals(CONTAINER_XML)) |
| parseContainerXML(); |
| } |
| |
| public void addResource(URI uri, String path, String mediaType) |
| throws IOException { |
| try { |
| odfPackage.insert(uri, path, mediaType); |
| } catch (IOException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new IOException("Could not add " + path, e); |
| } |
| |
| if (path.equals(CONTAINER_XML)) |
| parseContainerXML(); |
| } |
| |
| public String getResourceAsString(String path) throws IOException { |
| try { |
| return new String(odfPackage.getBytes(path), UTF_8); |
| } catch (IOException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new IOException("Could not get " + path, e); |
| } |
| } |
| |
| public byte[] getResourceAsBytes(String path) throws IOException { |
| try { |
| return odfPackage.getBytes(path); |
| } catch (IOException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new IOException("Could not get " + path, e); |
| } |
| } |
| |
| public InputStream getResourceAsInputStream(String path) throws IOException { |
| try { |
| return odfPackage.getInputStream(path); |
| } catch (IOException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new IOException("Could not get " + path, e); |
| } |
| } |
| |
| public Map<String, ResourceEntry> listResources() { |
| return listResources("", false); |
| } |
| |
| public Map<String, ResourceEntry> listResources(String folderPath) { |
| return listResources(folderPath, false); |
| } |
| |
| protected Map<String, ResourceEntry> listResources(String folderPath, |
| boolean recursive) { |
| if (!folderPath.isEmpty() && !folderPath.endsWith("/")) |
| folderPath = folderPath + "/"; |
| HashMap<String, ResourceEntry> content = new HashMap<>(); |
| |
| for (Entry<String, OdfFileEntry> entry : odfPackage |
| .getManifestEntries().entrySet()) { |
| String entryPath = entry.getKey(); |
| if (!entryPath.startsWith(folderPath)) |
| continue; |
| String subPath = entryPath.substring(folderPath.length(), |
| entryPath.length()); |
| if (subPath.isEmpty()) |
| // The folder itself |
| continue; |
| int firstSlash = subPath.indexOf("/"); |
| if (!recursive && firstSlash > -1 |
| && firstSlash < subPath.length() - 1) |
| /* |
| * Children of a folder (note that we'll include the folder |
| * itself which ends in /) |
| */ |
| continue; |
| content.put(subPath, new ResourceEntry(entry.getValue())); |
| } |
| return content; |
| } |
| |
| public void removeResource(String path) { |
| if (!odfPackage.contains(path)) |
| return; |
| if (path.endsWith("/")) |
| for (ResourceEntry childEntry : listResources(path).values()) |
| removeResource(childEntry.getPath()); |
| odfPackage.remove(path); |
| } |
| |
| public class ResourceEntry { |
| private final String path; |
| private final long size; |
| private String mediaType; |
| private String version; |
| |
| protected ResourceEntry(OdfFileEntry odfEntry) { |
| path = odfEntry.getPath(); |
| size = odfEntry.getSize(); |
| mediaType = odfEntry.getMediaType(); |
| version = odfEntry.getVersion(); |
| } |
| |
| public String getPath() { |
| return path; |
| } |
| |
| public long getSize() { |
| return size; |
| } |
| |
| public String getMediaType() { |
| return mediaType; |
| } |
| |
| public boolean isFolder() { |
| return path.endsWith("/"); |
| } |
| |
| public UCFPackage getUcfPackage() { |
| return UCFPackage.this; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (!(obj instanceof ResourceEntry)) |
| return false; |
| ResourceEntry other = (ResourceEntry) obj; |
| |
| if (!getUcfPackage().equals(other.getUcfPackage())) |
| return false; |
| return getPath().equals(other.getPath()); |
| } |
| |
| @Override |
| public int hashCode() { |
| final int prime = 31; |
| int result = 1; |
| result = prime * result + getUcfPackage().hashCode(); |
| result = prime * result + ((path == null) ? 0 : path.hashCode()); |
| return result; |
| } |
| |
| public String getVersion() { |
| return version; |
| } |
| } |
| |
| public Map<String, ResourceEntry> listAllResources() { |
| return listResources("", true); |
| } |
| |
| public void setRootFile(String path) { |
| setRootFile(path, null); |
| } |
| |
| @SuppressWarnings("rawtypes") |
| public void setRootFile(String path, String version) { |
| ResourceEntry rootFile = getResourceEntry(path); |
| if (rootFile == null) |
| throw new IllegalArgumentException("Unknown resource: " + path); |
| odfPackage.getManifestEntries().get(path).setVersion(version); |
| |
| Container container = containerXml.getValue(); |
| |
| RootFiles rootFiles = getRootFiles(container); |
| String mediaType = rootFile.getMediaType(); |
| boolean foundExisting = false; |
| // Check any existing files for matching path/mime type |
| Iterator<Object> anyOrRootIt = rootFiles.getAnyOrRootFile().iterator(); |
| while (anyOrRootIt.hasNext()) { |
| Object anyOrRoot = anyOrRootIt.next(); |
| if (anyOrRoot instanceof JAXBElement) |
| anyOrRoot = ((JAXBElement) anyOrRoot).getValue(); |
| if (!(anyOrRoot instanceof RootFile)) |
| continue; |
| RootFile rootFileElem = (RootFile) anyOrRoot; |
| if (!rootFileElem.getFullPath().equals(path) |
| && !rootFileElem.getMediaType().equals(mediaType)) |
| // Different path and media type - ignore |
| continue; |
| if (foundExisting) { |
| // Duplicate path/media type, we'll remove it |
| anyOrRootIt.remove(); |
| continue; |
| } |
| rootFileElem.setFullPath(rootFile.getPath()); |
| if (mediaType != null) |
| rootFileElem.setMediaType(mediaType); |
| |
| foundExisting = true; |
| } |
| if (!foundExisting) { |
| RootFile rootFileElem = containerFactory.createRootFile(); |
| rootFileElem.setFullPath(rootFile.getPath()); |
| rootFileElem.setMediaType(mediaType); |
| rootFiles.getAnyOrRootFile().add( |
| containerFactory |
| .createContainerRootFilesRootFile(rootFileElem)); |
| // rootFiles.getAnyOrRootFile().add(rootFileElem); |
| } |
| } |
| |
| protected RootFiles getRootFiles(Container container) { |
| for (Object o : container.getRootFilesOrAny()) { |
| if (o instanceof JAXBElement) { |
| @SuppressWarnings("rawtypes") |
| JAXBElement jaxbElement = (JAXBElement) o; |
| o = jaxbElement.getValue(); |
| } |
| if (o instanceof RootFiles) |
| return (RootFiles) o; |
| } |
| // Not found - add it |
| RootFiles rootFiles = containerFactory.createContainerRootFiles(); |
| container.getRootFilesOrAny().add( |
| containerFactory.createContainerRootFiles(rootFiles)); |
| return rootFiles; |
| } |
| |
| @SuppressWarnings("rawtypes") |
| public List<ResourceEntry> getRootFiles() { |
| ArrayList<UCFPackage.ResourceEntry> rootFiles = new ArrayList<>(); |
| if (containerXml == null) |
| return rootFiles; |
| |
| RootFiles rootFilesElem = getRootFiles(containerXml.getValue()); |
| for (Object anyOrRoot : rootFilesElem.getAnyOrRootFile()) { |
| if (anyOrRoot instanceof JAXBElement) |
| anyOrRoot = ((JAXBElement) anyOrRoot).getValue(); |
| if (!(anyOrRoot instanceof RootFile)) |
| continue; |
| RootFile rf = (RootFile) anyOrRoot; |
| ResourceEntry entry = getResourceEntry(rf.getFullPath()); |
| if (rf.getMediaType() != null |
| && rf.getMediaType() != entry.mediaType) |
| // Override the mime type in the returned entry |
| entry.mediaType = rf.getMediaType(); |
| rootFiles.add(entry); |
| } |
| return rootFiles; |
| } |
| |
| public ResourceEntry getResourceEntry(String path) { |
| OdfFileEntry odfFileEntry = odfPackage.getManifestEntries().get(path); |
| if (odfFileEntry == null) |
| return null; |
| return new ResourceEntry(odfFileEntry); |
| } |
| |
| @SuppressWarnings("rawtypes") |
| public void unsetRootFile(String path) { |
| Container container = containerXml.getValue(); |
| RootFiles rootFiles = getRootFiles(container); |
| Iterator<Object> anyOrRootIt = rootFiles.getAnyOrRootFile().iterator(); |
| while (anyOrRootIt.hasNext()) { |
| Object anyOrRoot = anyOrRootIt.next(); |
| if (anyOrRoot instanceof JAXBElement) |
| anyOrRoot = ((JAXBElement) anyOrRoot).getValue(); |
| if (!(anyOrRoot instanceof RootFile)) |
| continue; |
| RootFile rootFileElem = (RootFile) anyOrRoot; |
| if (rootFileElem.getFullPath().equals(path)) |
| anyOrRootIt.remove(); |
| } |
| } |
| |
| protected JAXBElement<Container> getContainerXML() { |
| return containerXml; |
| } |
| |
| public void save(OutputStream output) throws IOException { |
| File tempFile = createTempFile("ucfpackage", ".tmp"); |
| prepareAndSave(tempFile); |
| |
| // Copy file to the output |
| |
| // Note - Should use IOUtils, but we're trying to avoid external dependencies |
| try (InputStream inStream = new FileInputStream(tempFile)) { |
| byte[] buffer = new byte[8192]; |
| int n = 0; |
| do { |
| output.write(buffer, 0, n); |
| n = inStream.read(buffer); |
| } while (n > -1); |
| } finally { |
| tempFile.delete(); |
| } |
| } |
| |
| public OutputStream addResourceUsingOutputStream(String path, |
| String mediaType) throws IOException { |
| if (path.equals(CONTAINER_XML)) |
| // as we need to parse it after insertion, this must fail |
| throw new IllegalArgumentException("Can't add " + CONTAINER_XML |
| + " using OutputStream"); |
| try { |
| return odfPackage.insertOutputStream(path, mediaType); |
| } catch (IOException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new IOException("Could not add " + path, e); |
| } |
| } |
| |
| @Override |
| public UCFPackage clone() { |
| final PipedOutputStream outputStream = new PipedOutputStream(); |
| try { |
| try (PipedInputStream inputStream = copyToOutputStream(outputStream)) { |
| return new UCFPackage(inputStream); |
| } |
| } catch (IOException e) { |
| throw new RuntimeException("Could not clone UCFPackage", e); |
| } |
| } |
| |
| private PipedInputStream copyToOutputStream( |
| final PipedOutputStream outputStream) throws IOException { |
| PipedInputStream inputStream = new PipedInputStream(outputStream); |
| new Thread("Cloning " + this) { |
| @Override |
| public void run() { |
| try { |
| try { |
| save(outputStream); |
| } finally { |
| outputStream.close(); |
| } |
| } catch (IOException e) { |
| logger.log(INFO, |
| "Could not save/close UCF package while cloning", e); |
| } |
| } |
| }.start(); |
| return inputStream; |
| } |
| |
| public String getRootFileVersion(String rootFile) { |
| return getResourceEntry(rootFile).getVersion(); |
| } |
| } |