001/*
002 * (C) Copyright 2006-2015 Nuxeo SA (http://nuxeo.com/) and others.
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 * Contributors:
017 *     Bogdan Stefanescu
018 *     Florent Guillaume
019 */
020package org.nuxeo.ecm.core.api.impl.blob;
021
022import java.io.BufferedInputStream;
023import java.io.Closeable;
024import java.io.File;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.Serializable;
028import java.util.ArrayList;
029import java.util.List;
030import java.util.zip.ZipEntry;
031import java.util.zip.ZipFile;
032import java.util.zip.ZipInputStream;
033
034import org.apache.commons.io.input.ProxyInputStream;
035import org.nuxeo.ecm.core.api.Blob;
036import org.nuxeo.ecm.core.api.CloseableFile;
037import org.nuxeo.ecm.core.api.NuxeoException;
038
039/**
040 * A {@link Blob} backed by an entry in a ZIP file.
041 *
042 * @since 7.2
043 */
044public class ZipEntryBlob extends AbstractBlob implements Serializable {
045
046    private static final long serialVersionUID = 1L;
047
048    protected final ZipFile zipFile;
049
050    protected final ZipEntry zipEntry;
051
052    protected final Blob zipBlob;
053
054    protected final String entryName;
055
056    /**
057     * Creates a {@link Blob} from an entry in a zip file. The {@link ZipFile} must not be closed until the stream has
058     * been read.
059     *
060     * @param zipFile the zip file
061     * @param zipEntry the zip entry
062     */
063    public ZipEntryBlob(ZipFile zipFile, ZipEntry zipEntry) {
064        this.zipFile = zipFile;
065        this.zipEntry = zipEntry;
066        zipBlob = null;
067        entryName = null;
068    }
069
070    /**
071     * Creates a {@link Blob} from an entry in a ZIP file blob.
072     *
073     * @param zipBlob the ZIP file blob
074     * @param entryName the ZIP entry name
075     * @since 11.5
076     */
077    public ZipEntryBlob(Blob zipBlob, String entryName) {
078        zipFile = null;
079        zipEntry = null;
080        this.zipBlob = zipBlob;
081        this.entryName = entryName;
082    }
083
084    @Override
085    public InputStream getStream() throws IOException {
086        if (zipBlob == null) {
087            return zipFile.getInputStream(zipEntry);
088        } else {
089            return ZipEntryInputStream.of(zipBlob, entryName);
090        }
091    }
092
093    @Override
094    public long getLength() {
095        if (zipBlob == null) {
096            return zipEntry.getSize();
097        } else {
098            File file = zipBlob.getFile();
099            if (file != null) {
100                // if there's a file then we can be fast
101                try (ZipFile zf = new ZipFile(file)) {
102                    ZipEntry entry = zf.getEntry(entryName);
103                    return entry == null ? 0 : entry.getSize();
104                } catch (IOException e) {
105                    throw new NuxeoException(e);
106                }
107            } else {
108                // else use the stream
109                try (ZipInputStream zin = new ZipInputStream(new BufferedInputStream(zipBlob.getStream()))) {
110                    ZipEntry entry;
111                    while ((entry = zin.getNextEntry()) != null) {
112                        if (entry.getName().equals(entryName)) {
113                            return entry.getSize();
114                        }
115                    }
116                    return 0;
117                } catch (IOException e) {
118                    throw new NuxeoException(e);
119                }
120            }
121        }
122    }
123
124    // @since 11.5
125    @Override
126    public String getFilename() {
127        return entryName != null ? entryName : super.getFilename();
128    }
129
130    /**
131     * {@link InputStream} for a ZIP entry, that closes all necessary resources when {@linkplain #close closed}.
132     *
133     * @since 11.5
134     */
135    public static class ZipEntryInputStream extends ProxyInputStream {
136
137        // what we'll need to close in addition to the InputStream itself
138        protected final List<Closeable> closeables;
139
140        /**
141         * Factory for a {@link ZipEntryInputStream}.
142         */
143        public static ZipEntryInputStream of(Blob zipBlob, String entryName) throws IOException {
144            List<Closeable> closeables = new ArrayList<>(2);
145            InputStream in;
146            try {
147                CloseableFile closeableFile = zipBlob.getCloseableFile();
148                closeables.add(closeableFile);
149                ZipFile zipFile = new ZipFile(closeableFile.getFile());
150                closeables.add(zipFile);
151                in = zipFile.getInputStream(zipFile.getEntry(entryName));
152            } catch (IOException ioe) {
153                try {
154                    close(closeables);
155                } catch (IOException e) {
156                    ioe.addSuppressed(e);
157                }
158                throw ioe;
159            }
160            return new ZipEntryInputStream(in, closeables);
161        }
162
163        protected ZipEntryInputStream(InputStream in, List<Closeable> closeables) {
164            super(in);
165            this.closeables = closeables;
166        }
167
168        @Override
169        public void close() throws IOException {
170            List<Closeable> closing = new ArrayList<>(3);
171            closing.add(super::close);
172            closing.addAll(closeables);
173            closeables.clear();
174            close(closing);
175        }
176
177        protected static void close(List<Closeable> closeables) throws IOException {
178            IOException ioe = null;
179            for (Closeable closeable : closeables) {
180                try {
181                    closeable.close();
182                } catch (IOException e) {
183                    if (ioe == null) {
184                        ioe = e;
185                    } else {
186                        ioe.addSuppressed(e);
187                    }
188                }
189            }
190            if (ioe != null) {
191                throw ioe;
192            }
193        }
194    }
195
196}