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