001/*
002 * Copyright 2012-2016 the original author or authors.
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.springframework.boot.loader.archive;
018
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.IOException;
022import java.net.MalformedURLException;
023import java.net.URL;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.Collections;
027import java.util.Comparator;
028import java.util.Deque;
029import java.util.HashSet;
030import java.util.Iterator;
031import java.util.LinkedList;
032import java.util.List;
033import java.util.NoSuchElementException;
034import java.util.Set;
035import java.util.jar.Manifest;
036
037/**
038 * {@link Archive} implementation backed by an exploded archive directory.
039 *
040 * @author Phillip Webb
041 * @author Andy Wilkinson
042 */
043public class ExplodedArchive implements Archive {
044
045        private static final Set<String> SKIPPED_NAMES = new HashSet<String>(
046                        Arrays.asList(".", ".."));
047
048        private final File root;
049
050        private final boolean recursive;
051
052        private File manifestFile;
053
054        private Manifest manifest;
055
056        /**
057         * Create a new {@link ExplodedArchive} instance.
058         * @param root the root folder
059         */
060        public ExplodedArchive(File root) {
061                this(root, true);
062        }
063
064        /**
065         * Create a new {@link ExplodedArchive} instance.
066         * @param root the root folder
067         * @param recursive if recursive searching should be used to locate the manifest.
068         * Defaults to {@code true}, folders with a large tree might want to set this to
069         * {@code
070         * false}.
071         */
072        public ExplodedArchive(File root, boolean recursive) {
073                if (!root.exists() || !root.isDirectory()) {
074                        throw new IllegalArgumentException("Invalid source folder " + root);
075                }
076                this.root = root;
077                this.recursive = recursive;
078                this.manifestFile = getManifestFile(root);
079        }
080
081        private File getManifestFile(File root) {
082                File metaInf = new File(root, "META-INF");
083                return new File(metaInf, "MANIFEST.MF");
084        }
085
086        @Override
087        public URL getUrl() throws MalformedURLException {
088                return this.root.toURI().toURL();
089        }
090
091        @Override
092        public Manifest getManifest() throws IOException {
093                if (this.manifest == null && this.manifestFile.exists()) {
094                        FileInputStream inputStream = new FileInputStream(this.manifestFile);
095                        try {
096                                this.manifest = new Manifest(inputStream);
097                        }
098                        finally {
099                                inputStream.close();
100                        }
101                }
102                return this.manifest;
103        }
104
105        @Override
106        public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
107                List<Archive> nestedArchives = new ArrayList<Archive>();
108                for (Entry entry : this) {
109                        if (filter.matches(entry)) {
110                                nestedArchives.add(getNestedArchive(entry));
111                        }
112                }
113                return Collections.unmodifiableList(nestedArchives);
114        }
115
116        @Override
117        public Iterator<Entry> iterator() {
118                return new FileEntryIterator(this.root, this.recursive);
119        }
120
121        protected Archive getNestedArchive(Entry entry) throws IOException {
122                File file = ((FileEntry) entry).getFile();
123                return (file.isDirectory() ? new ExplodedArchive(file)
124                                : new JarFileArchive(file));
125        }
126
127        @Override
128        public String toString() {
129                try {
130                        return getUrl().toString();
131                }
132                catch (Exception ex) {
133                        return "exploded archive";
134                }
135        }
136
137        /**
138         * File based {@link Entry} {@link Iterator}.
139         */
140        private static class FileEntryIterator implements Iterator<Entry> {
141
142                private final Comparator<File> entryComparator = new EntryComparator();
143
144                private final File root;
145
146                private final boolean recursive;
147
148                private final Deque<Iterator<File>> stack = new LinkedList<Iterator<File>>();
149
150                private File current;
151
152                FileEntryIterator(File root, boolean recursive) {
153                        this.root = root;
154                        this.recursive = recursive;
155                        this.stack.add(listFiles(root));
156                        this.current = poll();
157                }
158
159                @Override
160                public boolean hasNext() {
161                        return this.current != null;
162                }
163
164                @Override
165                public Entry next() {
166                        if (this.current == null) {
167                                throw new NoSuchElementException();
168                        }
169                        File file = this.current;
170                        if (file.isDirectory()
171                                        && (this.recursive || file.getParentFile().equals(this.root))) {
172                                this.stack.addFirst(listFiles(file));
173                        }
174                        this.current = poll();
175                        String name = file.toURI().getPath()
176                                        .substring(this.root.toURI().getPath().length());
177                        return new FileEntry(name, file);
178                }
179
180                private Iterator<File> listFiles(File file) {
181                        File[] files = file.listFiles();
182                        if (files == null) {
183                                return Collections.<File>emptyList().iterator();
184                        }
185                        Arrays.sort(files, this.entryComparator);
186                        return Arrays.asList(files).iterator();
187                }
188
189                private File poll() {
190                        while (!this.stack.isEmpty()) {
191                                while (this.stack.peek().hasNext()) {
192                                        File file = this.stack.peek().next();
193                                        if (!SKIPPED_NAMES.contains(file.getName())) {
194                                                return file;
195                                        }
196                                }
197                                this.stack.poll();
198                        }
199                        return null;
200                }
201
202                @Override
203                public void remove() {
204                        throw new UnsupportedOperationException("remove");
205                }
206
207                /**
208                 * {@link Comparator} that orders {@link File} entries by their absolute paths.
209                 */
210                private static class EntryComparator implements Comparator<File> {
211
212                        @Override
213                        public int compare(File o1, File o2) {
214                                return o1.getAbsolutePath().compareTo(o2.getAbsolutePath());
215                        }
216
217                }
218
219        }
220
221        /**
222         * {@link Entry} backed by a File.
223         */
224        private static class FileEntry implements Entry {
225
226                private final String name;
227
228                private final File file;
229
230                FileEntry(String name, File file) {
231                        this.name = name;
232                        this.file = file;
233                }
234
235                public File getFile() {
236                        return this.file;
237                }
238
239                @Override
240                public boolean isDirectory() {
241                        return this.file.isDirectory();
242                }
243
244                @Override
245                public String getName() {
246                        return this.name;
247                }
248
249        }
250
251}