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

com.wavemaker.commons.zip.ZipResourceStore Maven / Gradle / Ivy

There is a newer version: 11.9.5.ee
Show newest version
/*******************************************************************************
 * Copyright (C) 2022-2023 WaveMaker, Inc.
 *
 * Licensed 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.
 ******************************************************************************/
package com.wavemaker.commons.zip;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.apache.commons.io.IOUtils;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;

import com.wavemaker.commons.MessageResource;
import com.wavemaker.commons.io.File;
import com.wavemaker.commons.io.Folder;
import com.wavemaker.commons.io.JailedResourcePath;
import com.wavemaker.commons.io.NoCloseInputStream;
import com.wavemaker.commons.io.Resource;
import com.wavemaker.commons.io.ResourcePath;
import com.wavemaker.commons.io.exception.ReadOnlyResourceException;
import com.wavemaker.commons.io.exception.ResourceException;
import com.wavemaker.commons.io.store.FileStore;
import com.wavemaker.commons.io.store.FolderStore;
import com.wavemaker.commons.io.store.ResourceStore;
import com.wavemaker.commons.io.store.StoredFile;
import com.wavemaker.commons.io.store.StoredFolder;
import com.wavemaker.commons.util.WMIOUtils;

/**
 * {@link ResourceStore}s for {@link ZipFile} and {@link ZipArchive}.
 *
 * @author Phillip Webb
 */
abstract class ZipResourceStore implements ResourceStore {

    private final JailedResourcePath path;

    private final ZipFile zipFile;

    public ZipResourceStore(ZipFile zipFile, JailedResourcePath path) {
        this.zipFile = zipFile;
        this.path = path;
    }

    protected ZipFile getZipFile() {
        return this.zipFile;
    }

    @Override
    public JailedResourcePath getPath() {
        return this.path;
    }

    @Override
    public Resource getExisting(JailedResourcePath path) {
        ZipFile.ZipFileDetailsEntry entry = getZipFile().getEntry(path, false);
        if (entry == null) {
            return null;
        }
        return entry.isFolder() ? getFolder(path) : getFile(path);
    }

    @Override
    public Folder getFolder(JailedResourcePath path) {
        final ZipFolderStore store = new ZipFolderStore(getZipFile(), path);
        return new StoredFolder() {

            @Override
            protected FolderStore getStore() {
                return store;
            }
        };
    }

    @Override
    public File getFile(JailedResourcePath path) {
        final ZipFileStore store = new ZipFileStore(getZipFile(), path);
        return new StoredFile() {

            @Override
            protected FileStore getStore() {
                return store;
            }
        };
    }

    @Override
    public boolean exists() {
        return getZipFile().getEntry(this.path, false) != null;
    }

    @Override
    public Resource rename(String name) {
        throw createReadOnlyException();
    }

    @Override
    public void delete() {
        throw createReadOnlyException();
    }

    @Override
    public void create() {
        throw createReadOnlyException();
    }

    @Override
    public int hashCode() {
        return this.zipFile.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        ZipResourceStore other = (ZipResourceStore) obj;
        return ObjectUtils.nullSafeEquals(this.zipFile, other.zipFile);
    }

    /**
     * create a new {@link ReadOnlyResourceException} that should be thrown on any write operations.
     *
     * @return the ReadOnlyResourceException exception to throw
     */
    protected final ReadOnlyResourceException createReadOnlyException() {
        throw new ReadOnlyResourceException(MessageResource.create("com.wavemaker.commons.readOnly.zip.file"), getZipFile());
    }

    static class ZipFileStore extends ZipResourceStore implements FileStore {

        public ZipFileStore(ZipFile zipFile, JailedResourcePath path) {
            super(zipFile, path);
        }

        @Override
        public InputStream getInputStream() {
            try {
                return getZipFile().getEntry(getPath()).getInputStream();
            } catch (IOException e) {
                throw new ResourceException(e);
            }
        }

        @Override
        public OutputStream getOutputStream() {
            throw createReadOnlyException();
        }

        @Override
        public OutputStream getOutputStream(boolean append) {
            throw createReadOnlyException();
        }

        @Override
        public long getSize() {
            return getZipFile().getEntry(getPath()).getSize();
        }

        @Override
        public long getLastModified() {
            return getZipFile().getLastModified();
        }

        @Override
        public void touch() {
            throw createReadOnlyException();
        }
    }

    static class ZipFolderStore extends ZipResourceStore implements FolderStore {

        public ZipFolderStore(File zipFile) {
            this(new ZipFile(zipFile), new JailedResourcePath());
        }

        public ZipFolderStore(ZipFile zipFile, JailedResourcePath path) {
            super(zipFile, path);
        }

        @Override
        public Iterable list() {
            return getZipFile().getEntry(getPath()).list();
        }
    }

    private static class ZipFile {

        private final File file;

        private final Map entries = new HashMap<>();

        private boolean loadedAtLeastOnce;

        private long size;

        private long lastModified;

        public ZipFile(File file) {
            Assert.notNull(file, "ZipFile must not be null");
            Assert.isTrue(file.exists(), "ZipFile must exist");
            this.file = file;
        }

        public long getLastModified() {
            return this.file.getLastModified();
        }

        /**
         * Get {@link ZipFileDetailsEntry} for the specified key. This method will never return null.
         *
         * @param path the path to find
         *
         * @return the {@link ZipFileDetailsEntry}
         */
        private ZipFileDetailsEntry getEntry(JailedResourcePath path) {
            return getEntry(path, true);
        }

        /**
         * Get {@link ZipFileDetailsEntry} for the specified key if it exists.
         *
         * @param path     the path to find
         * @param required if the entry must exist
         *
         * @return the {@link ZipFileDetailsEntry}
         */
        private ZipFileDetailsEntry getEntry(JailedResourcePath path, boolean required) {
            if (isUnderlyingZipFileChanged()) {
                try {
                    reloadZipFile();
                } catch (IOException e) {
                    throw new ResourceException(e);
                }
            }
            ResourcePath unjailedPath = path.getUnjailedPath();
            ZipFileDetailsEntry entry = this.entries.get(unjailedPath);
            if (entry == null && required) {
                return new MissingZipFileDetailsEntry(unjailedPath);
            }
            return entry;
        }

        /**
         * Determine if the underlying zip file has changed.
         *
         * @return if the underlying file has changed.
         */
        private boolean isUnderlyingZipFileChanged() {
            return !this.loadedAtLeastOnce || this.size != this.file
                .getSize() || this.lastModified != this.file.getLastModified();
        }

        private void reloadZipFile() throws IOException {
            addZipEntries();
            createMissingFolderEntries();
            this.size = this.file.getSize();
            this.lastModified = this.file.getLastModified();
            this.loadedAtLeastOnce = true;
        }

        protected final ZipInputStream openZipInputStream() {
            return new ZipInputStream(new BufferedInputStream(this.file.getContent().asInputStream()));
        }

        /**
         * Add entries from the zip file.
         */
        private void addZipEntries() throws IOException {
            ZipInputStream zipInputStream = openZipInputStream();
            try {
                ZipEntry entry = zipInputStream.getNextEntry();
                while (entry != null) {
                    ResourcePath path = new ResourcePath().get(entry.getName());
                    this.entries.put(path, new ZipEntryZipFileDetailsEntry(path, entry));
                    entry = zipInputStream.getNextEntry();
                }
            } finally {
                zipInputStream.close();
            }
        }

        /**
         * Create any missing folder entries. This can occur if a zip file does include entries for parent folders.
         */
        private void createMissingFolderEntries() {
            List missingEntries = new ArrayList<>();
            Set paths = this.entries.keySet();
            for (ResourcePath path : paths) {
                path = path.getParent();
                while (path != null) {
                    if (!this.entries.containsKey(path)) {
                        missingEntries.add(new MissingZipFileDetailsEntry(path));
                    }
                    path = path.getParent();
                }
            }
            for (MissingZipFileDetailsEntry missingEntry : missingEntries) {
                this.entries.put(missingEntry.getPath(), missingEntry);
            }
        }

        @Override
        public int hashCode() {
            return this.file.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == this) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            ZipFile other = (ZipFile) obj;
            return ObjectUtils.nullSafeEquals(this.file, other.file);
        }

        /**
         * Details for a single entry from the {@link }.
         */
        private abstract class ZipFileDetailsEntry {

            private List list;

            private final ResourcePath path;

            public ZipFileDetailsEntry(ResourcePath path) {
                this.path = path;
            }

            public abstract boolean isFolder();

            public abstract InputStream getInputStream() throws IOException;

            public abstract long getSize();

            public Iterable list() {
                if (this.list == null) {
                    this.list = new ArrayList<>();
                    if (isFolder()) {
                        for (ResourcePath entryPath : ZipFile.this.entries.keySet()) {
                            if (isParent(entryPath)) {
                                this.list.add(entryPath.getName());
                            }
                        }
                    }
                }
                return this.list;
            }

            private boolean isParent(ResourcePath path) {
                return path != null && this.path.equals(path.getParent());
            }

            public ResourcePath getPath() {
                return this.path;
            }
        }

        private class MissingZipFileDetailsEntry extends ZipFileDetailsEntry {

            public MissingZipFileDetailsEntry(ResourcePath path) {
                super(path);
            }

            @Override
            public boolean isFolder() {
                return true;
            }

            @Override
            public InputStream getInputStream() {
                return null;
            }

            @Override
            public long getSize() {
                return 0;
            }
        }

        private class ZipEntryZipFileDetailsEntry extends ZipFileDetailsEntry {

            private final ZipEntry entry;

            private WeakReference contents;

            public ZipEntryZipFileDetailsEntry(ResourcePath path, ZipEntry entry) {
                super(path);
                this.entry = entry;
            }

            @Override
            public boolean isFolder() {
                return this.entry.isDirectory();
            }

            @Override
            public InputStream getInputStream() throws IOException {
                byte[] bytes = this.contents == null ? null : this.contents.get();
                try {
                    if (bytes == null) {
                        bytes = getZipEntryBytes();
                        this.contents = new WeakReference<>(bytes);
                    }
                    return new ByteArrayInputStream(bytes);
                } finally {
                    this.contents = null;
                }
            }

            private byte[] getZipEntryBytes() throws IOException {
                ZipInputStream zipInputStream = openZipInputStream();
                try {
                    ZipEntry zipEntry = zipInputStream.getNextEntry();
                    while (zipEntry != null) {
                        if (!zipEntry.isDirectory()) {
                            ResourcePath path = new ResourcePath().get(zipEntry.getName());
                            if (getPath().equals(path)) {
                                return IOUtils.toByteArray(new NoCloseInputStream(zipInputStream));
                            }
                        }
                        zipEntry = zipInputStream.getNextEntry();
                    }
                    throw new IllegalStateException("Unable to find ZipEntry for " + getPath());
                } finally {
                    WMIOUtils.closeSilently(zipInputStream);
                }
            }

            @Override
            public long getSize() {
                return this.entry.getSize();
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy