001/*
002 * ModeShape (http://www.modeshape.org)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *       http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.modeshape.sequencer.zip;
018
019import java.io.ByteArrayOutputStream;
020import java.io.IOException;
021import java.util.zip.ZipEntry;
022import java.util.zip.ZipInputStream;
023import javax.jcr.Binary;
024import javax.jcr.NamespaceRegistry;
025import javax.jcr.Node;
026import javax.jcr.PathNotFoundException;
027import javax.jcr.Property;
028import javax.jcr.RepositoryException;
029import org.modeshape.common.util.CheckArg;
030import org.modeshape.jcr.api.JcrConstants;
031import org.modeshape.jcr.api.nodetype.NodeTypeManager;
032import org.modeshape.jcr.api.sequencer.Sequencer;
033
034/**
035 * A sequencer that processes and extract the files and folders from ZIP archive files.
036 * 
037 * @author Horia Chiorean
038 */
039public class ZipSequencer extends Sequencer {
040
041    public static final class MimeTypeConstants {
042        public static final String JAR = "application/java-archive";
043        public static final String ZIP = "application/zip";
044    }
045
046    @Override
047    public void initialize( NamespaceRegistry registry,
048                            NodeTypeManager nodeTypeManager ) throws RepositoryException, IOException {
049        super.registerNodeTypes("zip.cnd", nodeTypeManager, true);
050        registerDefaultMimeTypes(MimeTypeConstants.JAR, MimeTypeConstants.ZIP);
051    }
052
053    @Override
054    public boolean execute( Property inputProperty,
055                            Node outputNode,
056                            Context context ) throws Exception {
057        Binary binaryValue = inputProperty.getBinary();
058        CheckArg.isNotNull(binaryValue, "binary");
059
060        try (ZipInputStream zipInputStream = new ZipInputStream(binaryValue.getStream())){
061            ZipEntry entry = zipInputStream.getNextEntry();
062            outputNode = createTopLevelNode(outputNode);
063            while (entry != null) {
064                entry = sequenceZipEntry(outputNode, context, zipInputStream, entry);
065            }
066            return true;
067        }
068    }
069
070    private Node createTopLevelNode( Node outputNode ) throws RepositoryException {
071        // Create top-level node
072        if (!outputNode.isNew()) {
073            outputNode = outputNode.addNode(ZipLexicon.CONTENT);
074        }
075        outputNode.setPrimaryType(ZipLexicon.FILE);
076        return outputNode;
077    }
078
079    private ZipEntry sequenceZipEntry( Node outputNode,
080                                       Context context,
081                                       ZipInputStream zipInputStream,
082                                       ZipEntry entry ) throws RepositoryException, IOException {
083        Node zipEntryNode = createZipEntryPath(outputNode, entry);
084
085        if (!entry.isDirectory()) {
086            addFileContent(zipInputStream, entry, context, zipEntryNode);
087        }
088        return zipInputStream.getNextEntry();
089    }
090
091    private void addFileContent( ZipInputStream zipInputStream,
092                                 ZipEntry entry,
093                                 Context context,
094                                 Node zipFileNode ) throws RepositoryException, IOException {
095        Node contentNode = zipFileNode.addNode(JcrConstants.JCR_CONTENT, JcrConstants.NT_RESOURCE);
096        // on session pre-save the appropriate properties should be set automatically
097        contentNode.addMixin(JcrConstants.MIX_LAST_MODIFIED);
098
099        // set the content bytes
100        byte[] contentBytes = readContent(zipInputStream);
101        org.modeshape.jcr.api.Binary contentBinary = context.valueFactory().createBinary(contentBytes);
102        contentNode.setProperty(JcrConstants.JCR_DATA, contentBinary);
103
104        // Figure out the mime type ...
105        String mimeType = contentBinary.getMimeType(entry.getName());
106        if (mimeType != null) {
107            contentNode.setProperty(JcrConstants.JCR_MIME_TYPE, mimeType);
108        }
109    }
110
111    /**
112     * Reads the content from the {@link ZipInputStream}, making sure it doesn't close the stream.
113     * 
114     * @param zipInputStream the input stream
115     * @return the content
116     * @throws IOException if there is a problem reading the stream
117     */
118    private byte[] readContent( ZipInputStream zipInputStream ) throws IOException {
119        int bufferLength = 1024;
120        byte[] buffer = new byte[bufferLength];
121        int n;
122        ByteArrayOutputStream baout = new ByteArrayOutputStream();
123        while ((n = zipInputStream.read(buffer, 0, bufferLength)) > -1) {
124            baout.write(buffer, 0, n);
125        }
126        return baout.toByteArray();
127    }
128
129    /**
130     * Creates (if necessary) the path from the {@link Node parentNode} to the {@link ZipEntry zip entry}, based on the name of
131     * the zip entry, which should contain its absolute path.
132     * 
133     * @param parentNode the parent node under which the node for the ZIP entry should be created
134     * @param entry the ZIP file entry
135     * @return the newly created node
136     * @throws RepositoryException if there is a problem writing the content to the repository session
137     */
138    private Node createZipEntryPath( Node parentNode,
139                                     ZipEntry entry ) throws RepositoryException {
140        Node zipEntryNode = parentNode;
141        String entryName = entry.getName();
142        String[] segments = entryName.split("/");
143        for (int i = 0; i < segments.length; i++) {
144            String segmentName = segments[i];
145            try {
146                zipEntryNode = zipEntryNode.getNode(segmentName);
147            } catch (PathNotFoundException e) {
148                // the path does not exist yet - create it
149                boolean isLastSegment = (i == segments.length - 1);
150                String segmentPrimaryType = isLastSegment ? (entry.isDirectory() ? JcrConstants.NT_FOLDER : JcrConstants.NT_FILE) : JcrConstants.NT_FOLDER;
151                zipEntryNode = zipEntryNode.addNode(segmentName, segmentPrimaryType);
152            }
153        }
154        return zipEntryNode;
155    }
156}