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 *     Nuxeo - initial API and implementation
018 */
019package org.nuxeo.osgi;
020
021import java.io.File;
022import java.io.FileNotFoundException;
023import java.io.IOException;
024import java.io.InputStream;
025import java.net.MalformedURLException;
026import java.net.URL;
027import java.util.ArrayList;
028import java.util.Collection;
029import java.util.Collections;
030import java.util.Enumeration;
031import java.util.List;
032import java.util.jar.Attributes;
033import java.util.jar.JarEntry;
034import java.util.jar.JarFile;
035import java.util.jar.Manifest;
036import java.util.zip.ZipEntry;
037
038import org.apache.commons.io.FileUtils;
039import org.apache.commons.logging.Log;
040import org.apache.commons.logging.LogFactory;
041import org.nuxeo.common.utils.StringUtils;
042import org.nuxeo.osgi.util.EntryFilter;
043import org.osgi.framework.Constants;
044
045/**
046 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
047 */
048public class JarBundleFile implements BundleFile {
049
050    private static final Log log = LogFactory.getLog(JarBundleFile.class);
051
052    protected JarFile jarFile;
053
054    protected String urlBase;
055
056    @SuppressWarnings("resource")
057    public JarBundleFile(File file) throws IOException {
058        this(new JarFile(file));
059    }
060
061    public JarBundleFile(JarFile jarFile) {
062        this.jarFile = jarFile;
063        try {
064            urlBase = "jar:" + new File(jarFile.getName()).toURI().toURL() + "!/";
065        } catch (MalformedURLException e) {
066            log.error("Failed to convert bundle location to an URL: " + jarFile.getName()
067                    + ". Bundle getEntry will not work.", e);
068        }
069    }
070
071    @Override
072    public Enumeration<URL> findEntries(String name, String pattern, boolean recurse) {
073        if (name.startsWith("/")) {
074            name = name.substring(1);
075        }
076        String prefix;
077        if (name.length() == 0) {
078            name = null;
079            prefix = "";
080        } else if (!name.endsWith("/")) {
081            prefix = name + "/";
082        } else {
083            prefix = name;
084        }
085        int len = prefix.length();
086        EntryFilter filter = EntryFilter.newFilter(pattern);
087        Enumeration<JarEntry> entries = jarFile.entries();
088        ArrayList<URL> result = new ArrayList<>();
089        try {
090            while (entries.hasMoreElements()) {
091                JarEntry entry = entries.nextElement();
092                if (entry.isDirectory()) {
093                    continue;
094                }
095                String ename = entry.getName();
096                if (name != null && !ename.startsWith(prefix)) {
097                    continue;
098                }
099                int i = ename.lastIndexOf('/');
100                if (!recurse) {
101                    if (i > -1) {
102                        if (ename.indexOf('/', len) > -1) {
103                            continue;
104                        }
105                    }
106                }
107                String n = i > -1 ? ename.substring(i + 1) : ename;
108                if (filter.match(n)) {
109                    result.add(getEntryUrl(ename));
110                }
111            }
112        } catch (MalformedURLException e) {
113            throw new RuntimeException(e);
114        }
115        return Collections.enumeration(result);
116    }
117
118    @Override
119    public URL getEntry(String name) {
120        ZipEntry entry = jarFile.getEntry(name);
121        if (entry == null) {
122            return null;
123        }
124        if (name.startsWith("/")) {
125            name = name.substring(1);
126        }
127        try {
128            return new URL(urlBase + name);
129        } catch (MalformedURLException e) {
130            return null;
131        }
132    }
133
134    @Override
135    public Enumeration<String> getEntryPaths(String path) {
136        throw new UnsupportedOperationException("The operation BundleFile.geEntryPaths() was not yet implemented");
137    }
138
139    @Override
140    public File getFile() {
141        return new File(jarFile.getName());
142    }
143
144    @Override
145    public String getFileName() {
146        String path = jarFile.getName();
147        int punix = path.lastIndexOf('/');
148        int pwin = path.lastIndexOf('\\');
149        int p = punix > pwin ? punix : pwin;
150        if (p == -1) {
151            return path;
152        }
153        if (p == 0) {
154            return "";
155        }
156        return path.substring(p + 1);
157    }
158
159    @Override
160    public String getLocation() {
161        return jarFile.getName();
162    }
163
164    @Override
165    public Manifest getManifest() {
166        try {
167            return jarFile.getManifest();
168        } catch (IOException e) {
169            return null;
170        }
171    }
172
173    @Override
174    public Collection<BundleFile> getNestedBundles(File tmpDir) throws IOException {
175        Attributes attrs = jarFile.getManifest().getMainAttributes();
176        String cp = attrs.getValue(Constants.BUNDLE_CLASSPATH);
177        if (cp == null) {
178            cp = attrs.getValue("Class-Path");
179        }
180        if (cp == null) {
181            return null;
182        }
183        String[] paths = StringUtils.split(cp, ',', true);
184        URL base = new URL("jar:" + new File(jarFile.getName()).toURI().toURL().toExternalForm() + "!/");
185        String fileName = getFileName();
186        List<BundleFile> nested = new ArrayList<>();
187        for (String path : paths) {
188            if (path.equals(".")) {
189                continue; // TODO
190            }
191            String location = base + path;
192            String name = path.replace('/', '_');
193            File dest = new File(tmpDir, fileName + '-' + name);
194            try {
195                extractNestedJar(jarFile, path, dest);
196                nested.add(new NestedJarBundleFile(location, dest));
197            } catch (FileNotFoundException e) {
198                log.error("A nested jar is referenced in manifest but not found: " + location);
199            } catch (IOException e) {
200                log.error(e);
201            }
202        }
203        return nested;
204    }
205
206    public static void extractNestedJar(JarFile file, String path, File dest) throws IOException {
207        extractNestedJar(file, file.getEntry(path), dest);
208    }
209
210    public static void extractNestedJar(JarFile file, ZipEntry entry, File dest) throws IOException {
211        try (InputStream in = file.getInputStream(entry)) {
212            FileUtils.copyInputStreamToFile(in, dest);
213        }
214    }
215
216    @Override
217    public Collection<BundleFile> findNestedBundles(File tmpDir) throws IOException {
218        URL base = new URL("jar:" + new File(jarFile.getName()).toURI().toURL().toExternalForm() + "!/");
219        String fileName = getFileName();
220        Enumeration<JarEntry> entries = jarFile.entries();
221        List<BundleFile> nested = new ArrayList<>();
222        while (entries.hasMoreElements()) {
223            JarEntry entry = entries.nextElement();
224            String path = entry.getName();
225            if (entry.getName().endsWith(".jar")) {
226                String location = base + path;
227                String name = path.replace('/', '_');
228                File dest = new File(tmpDir, fileName + '-' + name);
229                extractNestedJar(jarFile, entry, dest);
230                nested.add(new NestedJarBundleFile(location, dest));
231            }
232        }
233        return nested;
234    }
235
236    @Override
237    public String getSymbolicName() {
238        try {
239            String value = jarFile.getManifest().getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
240            return value != null ? BundleManifestReader.removePropertiesFromHeaderValue(value) : null;
241        } catch (IOException e) {
242            return null;
243        }
244    }
245
246    @Override
247    public URL getURL() {
248        try {
249            return new File(jarFile.getName()).toURI().toURL();
250        } catch (MalformedURLException e) {
251            return null;
252        }
253    }
254
255    public URL getJarURL() {
256        try {
257            String url = new File(jarFile.getName()).toURI().toURL().toExternalForm();
258            return new URL("jar:" + url + "!/");
259        } catch (MalformedURLException e) {
260            return null;
261        }
262    }
263
264    @Override
265    public boolean isDirectory() {
266        return false;
267    }
268
269    @Override
270    public boolean isJar() {
271        return true;
272    }
273
274    @Override
275    public String toString() {
276        return getLocation();
277    }
278
279    protected final URL getEntryUrl(String name) throws MalformedURLException {
280        return new URL(urlBase + name);
281    }
282
283    @Override
284    public void close() throws IOException {
285        if (jarFile == null) {
286            return;
287        }
288        try {
289            jarFile.close();
290        } finally {
291            jarFile = null;
292        }
293    }
294
295}