001/*
002 * Copyright 2011-2012 Stephen Connolly.
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.jszip.pseudo.io;
018
019import org.apache.commons.lang3.StringUtils;
020import org.codehaus.plexus.archiver.zip.ZipEntry;
021import org.codehaus.plexus.archiver.zip.ZipFile;
022import org.mozilla.javascript.Context;
023
024import java.io.File;
025import java.io.IOException;
026import java.util.ArrayList;
027import java.util.Arrays;
028import java.util.Collections;
029import java.util.Enumeration;
030import java.util.LinkedHashSet;
031import java.util.List;
032import java.util.Map;
033import java.util.Set;
034import java.util.TreeMap;
035
036public class PseudoFileSystem {
037    /**
038     * Secret key used to hold the reference to the pseudo filesystem.
039     */
040    private static final Object KEY = new Object();
041
042    private final PseudoFile root = new VirtualDirectoryPseudoFile(null, "");
043
044    private final Layer[] layers;
045
046    public PseudoFileSystem(Layer... layers) {
047        this.layers = layers;
048    }
049
050    public PseudoFileSystem(List<Layer> layers) {
051        this(layers.toArray(new Layer[layers.size()]));
052    }
053
054    public String getPathSeparator() {
055        return "/";
056    }
057
058    public PseudoFile[] listChildren(PseudoFile dir, PseudoFileFilter filter) {
059        TreeMap<String, Layer> names = new TreeMap<String, Layer>();
060        final String path = dir.getAbsolutePath(this);
061        for (int i = layers.length - 1; i >= 0; i--) {
062            for (String name : layers[i].listChildren(path)) {
063                names.put(name, layers[i]);
064            }
065        }
066        List<PseudoFile> result = new ArrayList<PseudoFile>(names.size());
067        for (Map.Entry<String, Layer> entry : names.entrySet()) {
068            if (filter.accept(entry.getKey())) {
069                result.add(entry.getValue().makeChild(this, dir, entry.getKey()));
070            }
071        }
072        return result.toArray(new PseudoFile[result.size()]);
073    }
074
075    public PseudoFile root() {
076        return root;
077    }
078
079    public PseudoFile getPseudoFile(String filename) {
080        filename = StringUtils.removeEnd(filename, getPathSeparator());
081        if (filename.isEmpty()) {
082            return root();
083        }
084        int index = filename.lastIndexOf(getPathSeparator());
085        if (index != -1) {
086            return getPseudoFile(getPseudoFile(filename.substring(0, index)), filename.substring(index + 1));
087        }
088        return getPseudoFile(root(), filename);
089    }
090
091    public PseudoFile getPseudoFile(PseudoFile parent, String name) {
092        if (name.equals(".")) {
093            return parent;
094        }
095        if (name.equals("..")) {
096            return parent.getParentFile();
097        }
098        String parentPath = parent.getAbsolutePath(this);
099        for (Layer layer : layers) {
100            if (layer.listChildren(parentPath).contains(name)) {
101                return layer.makeChild(this, parent, name);
102            }
103        }
104        if (layers.length == 0) {
105            return new VirtualDirectoryPseudoFile(parent, name);
106        }
107        return layers[0].makeChild(this, parent, name);
108    }
109
110    public synchronized void installInContext() {
111        Context.getCurrentContext().putThreadLocal(KEY, this);
112    }
113
114    public synchronized void removeFromContext() {
115        final Context context = Context.getCurrentContext();
116        if (context != null) {
117            context.putThreadLocal(KEY, null);
118        }
119    }
120
121    public static PseudoFileSystem current() {
122        final Context currentContext = Context.getCurrentContext();
123        return currentContext != null ? (PseudoFileSystem) currentContext.getThreadLocal(KEY) : null;
124    }
125
126    public abstract static class Layer {
127
128        public abstract List<String> listChildren(String relativePath);
129
130        public PseudoFile makeChild(PseudoFile parent, String name) {
131            return makeChild(PseudoFileSystem.current(), parent, name);
132        }
133
134        public abstract PseudoFile makeChild(PseudoFileSystem fs, PseudoFile parent, String name);
135
136    }
137
138    public static class FileLayer extends Layer {
139        private final String prefix;
140        private final File root;
141
142        public FileLayer(File root) {
143            this("", root);
144        }
145
146        public FileLayer(String prefix, File root) {
147            this.prefix = StringUtils.isEmpty(prefix) ? "/" : "/" + StringUtils.removeEnd(
148                    StringUtils.removeStart(prefix, "/"), "/") + "/";
149            this.root = root;
150        }
151
152        @Override
153        public List<String> listChildren(String relativePath) {
154            relativePath = StringUtils.removeEnd(relativePath, "/") + "/";
155            if (relativePath.startsWith(prefix)) {
156                final String pathFragment = relativePath.substring(prefix.length());
157                final String[] list = new File(root, pathFragment).list();
158                return list == null ? Collections.<String>emptyList() : Arrays.asList(list);
159            }
160            if (prefix.startsWith(relativePath)) {
161                int index = prefix.indexOf('/', relativePath.length() + 1);
162                if (index != -1) {
163                    return Collections.singletonList(prefix.substring(relativePath.length(), index));
164                }
165            }
166            return Collections.emptyList();
167        }
168
169        @Override
170        public PseudoFile makeChild(PseudoFileSystem fs, PseudoFile parent, String name) {
171            String relativePath = StringUtils.removeEnd(parent.getAbsolutePath(fs), "/") + "/" + name;
172            if (relativePath.startsWith(prefix)) {
173                return new FilePseudoFile(parent, new File(root, relativePath.substring(prefix.length())));
174            }
175            if (prefix.equals(relativePath + "/")) {
176                int lastIndex = prefix.lastIndexOf('/');
177                int index = prefix.lastIndexOf('/', lastIndex - 1);
178                return new AliasFilePseudoFile(parent, root, prefix.substring(index + 1, lastIndex));
179            }
180            if (!StringUtils.isEmpty(prefix) && prefix.startsWith(relativePath)) {
181                return new VirtualDirectoryPseudoFile(parent, name);
182            }
183            return new NotExistingPseudoFile(parent, name);
184        }
185
186        @Override
187        public String toString() {
188            final StringBuilder sb = new StringBuilder();
189            sb.append("FileLayer");
190            sb.append("{prefix='").append(prefix).append('\'');
191            sb.append(", root=").append(root);
192            sb.append('}');
193            return sb.toString();
194        }
195    }
196
197    public static class ZipLayer extends PseudoFileSystem.Layer {
198        private final String prefix;
199        private final File zipFile;
200        private final Map<String, ZipEntry> contents;
201
202        public ZipLayer(String prefix, File zipFile) throws IOException {
203            this.prefix = StringUtils.isEmpty(prefix) ? "/" : "/" + StringUtils.removeEnd(
204                    StringUtils.removeStart(prefix, "/"), "/") + "/";
205            this.zipFile = zipFile;
206            ZipFile file = new ZipFile(zipFile);
207            Map<String, ZipEntry> contents = new TreeMap<String, ZipEntry>();
208            Enumeration<ZipEntry> entries = file.getEntries();
209            while (entries.hasMoreElements()) {
210                ZipEntry entry = entries.nextElement();
211                contents.put(this.prefix + StringUtils.removeStart(entry.getName(), "/"), entry);
212            }
213
214            this.contents = contents;
215        }
216
217        @Override
218        public List<String> listChildren(String relativePath) {
219            relativePath = StringUtils.removeEnd(relativePath, "/") + "/";
220            if (relativePath.startsWith(prefix) || prefix.equals(relativePath + "/")) {
221                final String pathFragment = StringUtils.removeEnd(relativePath, "/") + "/";
222                Set<String> result = new LinkedHashSet<String>();
223                for (String path : contents.keySet()) {
224                    if (path.startsWith(pathFragment)) {
225                        int index = path.indexOf('/', pathFragment.length());
226                        if (index == -1) {
227                            result.add(path.substring(pathFragment.length()));
228                        } else {
229                            result.add(path.substring(pathFragment.length(), index));
230                        }
231                    }
232                }
233                return new ArrayList<String>(result);
234            }
235            if (prefix.startsWith(relativePath)) {
236                int index = prefix.indexOf('/', relativePath.length());
237                if (index != -1) {
238                    return Collections.singletonList(prefix.substring(relativePath.length(), index));
239                }
240            }
241            return Collections.emptyList();
242        }
243
244        @Override
245        public PseudoFile makeChild(PseudoFileSystem fs, PseudoFile parent, String name) {
246            String relativePath = StringUtils.removeEnd(parent.getAbsolutePath(fs), "/") + "/" + name;
247            final ZipEntry entry = contents.get(relativePath);
248            if (entry != null) {
249                return new ZipPseudoFile(parent, zipFile, entry);
250            }
251            if (prefix.equals(relativePath + "/")) {
252                return new VirtualDirectoryPseudoFile(parent, name);
253            }
254            if (!StringUtils.isEmpty(prefix) && prefix.startsWith(relativePath)) {
255                return new VirtualDirectoryPseudoFile(parent, name);
256            }
257            for (String childPath : contents.keySet()) {
258                if (!StringUtils.isEmpty(childPath) && childPath.startsWith(relativePath)) {
259                    return new VirtualDirectoryPseudoFile(parent, name);
260                }
261            }
262            return new NotExistingPseudoFile(parent, name);
263        }
264
265        @Override
266        public String toString() {
267            final StringBuilder sb = new StringBuilder();
268            sb.append("ZipLayer");
269            sb.append("{prefix='").append(prefix).append('\'');
270            sb.append(", zipFile=").append(zipFile);
271            sb.append('}');
272            return sb.toString();
273        }
274    }
275
276}