001/*
002 * (C) Copyright 2006-2016 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.osgi;
021
022import java.io.File;
023import java.io.FileInputStream;
024import java.io.IOException;
025import java.net.MalformedURLException;
026import java.net.URL;
027import java.util.ArrayList;
028import java.util.Collection;
029import java.util.Enumeration;
030import java.util.List;
031import java.util.jar.Attributes;
032import java.util.jar.Manifest;
033import java.util.stream.Collectors;
034
035import org.nuxeo.common.utils.FileUtils;
036import org.nuxeo.common.utils.StringUtils;
037import org.nuxeo.osgi.util.CompoundEnumeration;
038import org.nuxeo.osgi.util.EntryFilter;
039import org.nuxeo.osgi.util.FileIterator;
040import org.osgi.framework.Constants;
041
042/**
043 * A {@link BundleFile} that is backed by a filesystem directory, for use in test settings from Eclipse or maven.
044 */
045public class DirectoryBundleFile implements BundleFile {
046
047    public static final String MANIFEST_PATH = "META-INF/MANIFEST.MF";
048
049    protected final File file;
050
051    protected final List<File> files;
052
053    protected final Manifest mf;
054
055    public DirectoryBundleFile(File file) throws IOException {
056        this(file, null);
057    }
058
059    public DirectoryBundleFile(File file, Manifest mf) throws IOException {
060        this.file = file;
061        this.files = findFiles(file);
062        this.mf = mf == null ? findManifest() : mf;
063    }
064
065    protected List<File> findFiles(File file) {
066        List<File> files = new ArrayList<>(2);
067        files.add(file);
068        if (file.getPath().endsWith("/bin")) {
069            // hack for Eclipse PDE development
070            files.add(file.getParentFile());
071        } else if (file.getPath().endsWith("/target/classes")) {
072            // hack for maven/tycho development
073            files.add(file.getParentFile().getParentFile());
074        }
075        return files;
076    }
077
078    private Enumeration<URL> createEnumeration(File root, final EntryFilter efilter, final boolean recurse) {
079        FileIterator it = new FileIterator(root, pathname -> {
080            if (pathname.isDirectory()) {
081                return recurse;
082            }
083            return efilter.match(pathname.getName());
084        });
085        it.setSkipDirs(true);
086        return FileIterator.asUrlEnumeration(it);
087    }
088
089    @SuppressWarnings("unchecked")
090    @Override
091    public Enumeration<URL> findEntries(String name, String pattern, boolean recurse) {
092        EntryFilter efilter = EntryFilter.newFilter(pattern);
093        if (files.size() == 1) {
094            return createEnumeration(new File(file, name), efilter, recurse);
095        } else {
096            Enumeration<URL>[] enums = new Enumeration[files.size()];
097            int i = 0;
098            for (File f : files) {
099                enums[i++] = createEnumeration(new File(f, name), efilter, recurse);
100            }
101            return new CompoundEnumeration<>(enums);
102        }
103    }
104
105    @Override
106    public URL getEntry(String name) {
107        for (File file : files) {
108            File entry = new File(file, name);
109            if (entry.exists()) {
110                try {
111                    return entry.toURI().toURL();
112                } catch (MalformedURLException e) {
113                    // ignore
114                }
115            }
116        }
117        return null;
118    }
119
120    @Override
121    public Enumeration<String> getEntryPaths(String path) {
122        throw new UnsupportedOperationException("The operation BundleFile.geEntryPaths() is not yet implemented");
123    }
124
125    @Override
126    public File getFile() {
127        return file;
128    }
129
130    @Override
131    public String getFileName() {
132        return file.getName();
133    }
134
135    @Override
136    public String getLocation() {
137        return file.getPath();
138    }
139
140    @Override
141    public Manifest getManifest() {
142        return mf;
143    }
144
145    protected Manifest findManifest() throws IOException {
146        for (File file : files) {
147            File entry = new File(file, MANIFEST_PATH);
148            if (entry.exists()) {
149                try (FileInputStream fis = new FileInputStream(entry)) {
150                    return new Manifest(fis);
151                }
152            }
153        }
154        String paths = files.stream().map(File::toString).collect(Collectors.joining(", "));
155        throw new IOException(String.format("Could not find a file '%s' in paths: %s", MANIFEST_PATH, paths));
156    }
157
158    @Override
159    public Collection<BundleFile> getNestedBundles(File tmpDir) throws IOException {
160        Attributes attrs = mf.getMainAttributes();
161        String cp = attrs.getValue(Constants.BUNDLE_CLASSPATH);
162        if (cp == null) {
163            cp = attrs.getValue("Class-Path");
164        }
165        if (cp == null) {
166            return null;
167        }
168        String[] paths = StringUtils.split(cp, ',', true);
169        List<BundleFile> nested = new ArrayList<>();
170        for (String path : paths) {
171            File nestedBundle = new File(file, path);
172            if (nestedBundle.isDirectory()) {
173                nested.add(new DirectoryBundleFile(nestedBundle));
174            } else {
175                nested.add(new JarBundleFile(nestedBundle));
176            }
177        }
178        return nested;
179    }
180
181    @Override
182    public Collection<BundleFile> findNestedBundles(File tmpDir) throws IOException {
183        List<BundleFile> nested = new ArrayList<>();
184        File[] files = FileUtils.findFiles(file, "*.jar", true);
185        for (File jar : files) {
186            if (jar.isDirectory()) {
187                nested.add(new DirectoryBundleFile(jar));
188            } else {
189                nested.add(new JarBundleFile(jar));
190            }
191        }
192        return nested;
193    }
194
195    @Override
196    public String getSymbolicName() {
197        String value = mf.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
198        return value == null ? null : BundleManifestReader.removePropertiesFromHeaderValue(value);
199    }
200
201    @Override
202    public URL getURL() {
203        try {
204            return file.toURI().toURL();
205        } catch (MalformedURLException e) {
206            return null;
207        }
208    }
209
210    @Override
211    public boolean isDirectory() {
212        return true;
213    }
214
215    @Override
216    public boolean isJar() {
217        return false;
218    }
219
220    @Override
221    public String toString() {
222        return getLocation();
223    }
224
225    public static void main(String[] args) throws Exception {
226        DirectoryBundleFile bf = new DirectoryBundleFile(
227                new File("/Users/bstefanescu/work/org.eclipse.ecr/plugins/org.eclipse.ecr.application/bin"));
228        Enumeration<URL> urls = bf.findEntries("META-INF", "*.txt", false);
229        while (urls.hasMoreElements()) {
230            System.out.println(urls.nextElement());
231        }
232    }
233
234    @Override
235    public void close() {
236    }
237
238}