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