001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 *  Unless required by applicable law or agreed to in writing, software
012 *  distributed under the License is distributed on an "AS IS" BASIS,
013 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 *  See the License for the specific language governing permissions and
015 *  limitations under the License.
016 */
017package org.apache.xbean.finder.archive;
018
019import java.io.BufferedInputStream;
020import java.io.ByteArrayOutputStream;
021import java.io.File;
022import java.io.FileInputStream;
023import java.io.IOException;
024import java.io.InputStream;
025import java.net.URL;
026import java.util.ArrayList;
027import java.util.Iterator;
028import java.util.List;
029
030/**
031 * @version $Rev$ $Date$
032 */
033public class FileArchive implements Archive {
034
035    private final ClassLoader loader;
036    private final String basePackage;
037    private final File dir;
038    private List<String> list;
039    private final MJarSupport mjar = new MJarSupport();
040
041    public FileArchive(ClassLoader loader, URL url) {
042        this.loader = loader;
043        this.basePackage = "";
044        this.dir = toFile(url);
045    }
046
047    public FileArchive(ClassLoader loader, File dir) {
048        this.loader = loader;
049        this.basePackage = "";
050        this.dir = dir;
051    }
052
053    public FileArchive(ClassLoader loader, URL url, String basePackage) {
054        this.loader = loader;
055        this.basePackage = basePackage;
056        this.dir = toFile(url);
057    }
058
059    public FileArchive(ClassLoader loader, File dir, String basePackage) {
060        this.loader = loader;
061        this.basePackage = basePackage;
062        this.dir = dir;
063    }
064
065    public File getDir() {
066        return dir;
067    }
068
069    public InputStream getBytecode(String className) throws IOException, ClassNotFoundException {
070        int pos = className.indexOf("<");
071        if (pos > -1) {
072            className = className.substring(0, pos);
073        }
074        pos = className.indexOf(">");
075        if (pos > -1) {
076            className = className.substring(0, pos);
077        }
078        if (!className.endsWith(".class")) {
079            className = className.replace('.', '/') + ".class";
080        }
081
082        if (mjar.isMjar()) {
083            final MJarSupport.Clazz resource = mjar.getClasses().get(className);
084            if (resource != null) {
085                className = resource.getPath() + ".class";
086            }
087        }
088
089        URL resource = loader.getResource(className);
090        if (resource != null) return new BufferedInputStream(resource.openStream());
091
092        throw new ClassNotFoundException(className);
093    }
094
095
096    public Class<?> loadClass(String className) throws ClassNotFoundException {
097        // we assume the loader supports mjar if needed, do we want to wrap it to enforce it?
098        // probably not otherwise runtime will be weird and unexpected no?
099        return loader.loadClass(className);
100    }
101
102    public Iterator<Entry> iterator() {
103        return new ArchiveIterator(this, _iterator());
104    }
105
106    public Iterator<String> _iterator() {
107        if (list != null) return list.iterator();
108
109        final File manifest = new File(dir, "META-INF/MANIFEST.MF");
110        if (manifest.exists()) {
111            InputStream is = null;
112            try {
113                is =  new FileInputStream(manifest);
114                mjar.load(is);
115            } catch (final IOException e) {
116                // no-op
117            } finally {
118                if (is != null) {
119                    try {
120                        is.close();
121                    } catch (final IOException e) {
122                        // no-op
123                    }
124                }
125            }
126        }
127
128        list = file(dir);
129        return list.iterator();
130    }
131
132    private List<String> file(File dir) {
133        List<String> classNames = new ArrayList<String>();
134        if (dir.isDirectory()) {
135            scanDir(dir, classNames, (basePackage.length() > 0) ? (basePackage + ".") : basePackage);
136        }
137        return classNames;
138    }
139
140    private void scanDir(File dir, List<String> classNames, String packageName) {
141        File[] files = dir.listFiles();
142        // using /tmp/. as dir we can get null
143        if (files == null) {
144            return;
145        }
146        for (File file : files) {
147            if (file.isDirectory()) {
148                scanDir(file, classNames, packageName + file.getName() + ".");
149            } else if (file.getName().endsWith(".class")) {
150                String name = file.getName();
151                name = name.substring(0, name.length() - 6);
152                if (name.contains(".") || name.equals("module-info") /*todo?*/) continue;
153                if (packageName.startsWith("META-INF.versions")) {
154                    if (mjar.isMjar()) {
155                        mjar.visit(packageName + name);
156                        continue;
157                    }
158                }
159                classNames.add(packageName + name);
160            }
161        }
162    }
163
164    private static File toFile(URL url) {
165        if (!"file".equals(url.getProtocol())) throw new IllegalArgumentException("not a file url: " + url);
166        String path = url.getFile();
167        File dir = new File(decode(path));
168        if (dir.getName().equals("META-INF")) {
169            dir = dir.getParentFile(); // Scrape "META-INF" off
170        }
171        return dir;
172    }
173
174    public static String decode(String fileName) {
175        if (fileName.indexOf('%') == -1) return fileName;
176
177        StringBuilder result = new StringBuilder(fileName.length());
178        ByteArrayOutputStream out = new ByteArrayOutputStream();
179
180        for (int i = 0; i < fileName.length();) {
181            char c = fileName.charAt(i);
182
183            if (c == '%') {
184                out.reset();
185                do {
186                    if (i + 2 >= fileName.length()) {
187                        throw new IllegalArgumentException("Incomplete % sequence at: " + i);
188                    }
189
190                    int d1 = Character.digit(fileName.charAt(i + 1), 16);
191                    int d2 = Character.digit(fileName.charAt(i + 2), 16);
192
193                    if (d1 == -1 || d2 == -1) {
194                        throw new IllegalArgumentException("Invalid % sequence (" + fileName.substring(i, i + 3) + ") at: " + String.valueOf(i));
195                    }
196
197                    out.write((byte) ((d1 << 4) + d2));
198
199                    i += 3;
200
201                } while (i < fileName.length() && fileName.charAt(i) == '%');
202
203
204                result.append(out.toString());
205
206                continue;
207            } else {
208                result.append(c);
209            }
210
211            i++;
212        }
213        return result.toString();
214    }
215}