001/*
002 * Copyright (C) 2014 Jörg Prante
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 */
016package org.xbib.elasticsearch.plugin.jdbc.classloader.uri;
017
018import org.xbib.elasticsearch.plugin.jdbc.classloader.ResourceEnumeration;
019import org.xbib.elasticsearch.plugin.jdbc.classloader.ResourceFinder;
020import org.xbib.elasticsearch.plugin.jdbc.classloader.ResourceHandle;
021import org.xbib.elasticsearch.plugin.jdbc.classloader.ResourceLocation;
022import org.xbib.elasticsearch.plugin.jdbc.classloader.directory.DirectoryResourceLocation;
023import org.xbib.elasticsearch.plugin.jdbc.classloader.jar.JarResourceLocation;
024
025import java.io.File;
026import java.io.FileNotFoundException;
027import java.io.IOException;
028import java.net.MalformedURLException;
029import java.net.URI;
030import java.net.URISyntaxException;
031import java.net.URL;
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.Enumeration;
035import java.util.LinkedHashMap;
036import java.util.LinkedHashSet;
037import java.util.LinkedList;
038import java.util.List;
039import java.util.Map;
040import java.util.Set;
041import java.util.StringTokenizer;
042import java.util.jar.Attributes;
043import java.util.jar.Manifest;
044
045public class URIResourceFinder implements ResourceFinder {
046
047    private final Object lock = new Object();
048
049    private final Set<URI> uris = new LinkedHashSet<URI>();
050
051    private final Map<URI, ResourceLocation> classPath = new LinkedHashMap<URI, ResourceLocation>();
052
053    private final Set<File> watchedFiles = new LinkedHashSet<File>();
054
055    private boolean destroyed = false;
056
057    public URIResourceFinder() {
058    }
059
060    public void destroy() {
061        synchronized (lock) {
062            if (destroyed) {
063                return;
064            }
065            destroyed = true;
066            uris.clear();
067            for (ResourceLocation resourceLocation : classPath.values()) {
068                resourceLocation.close();
069            }
070            classPath.clear();
071        }
072    }
073
074    public ResourceHandle getResource(String resourceName) {
075        synchronized (lock) {
076            if (destroyed) {
077                return null;
078            }
079            Map<URI, ResourceLocation> path = getClassPath();
080            for (Map.Entry<URI, ResourceLocation> entry : path.entrySet()) {
081                ResourceLocation resourceLocation = entry.getValue();
082                ResourceHandle resourceHandle = resourceLocation.getResourceHandle(resourceName);
083                if (resourceHandle != null && !resourceHandle.isDirectory()) {
084                    return resourceHandle;
085                }
086            }
087        }
088        return null;
089    }
090
091    public URL findResource(String resourceName) {
092        synchronized (lock) {
093            if (destroyed) {
094                return null;
095            }
096            for (Map.Entry<URI, ResourceLocation> entry : getClassPath().entrySet()) {
097                ResourceLocation resourceLocation = entry.getValue();
098                ResourceHandle resourceHandle = resourceLocation.getResourceHandle(resourceName);
099                if (resourceHandle != null) {
100                    return resourceHandle.getUrl();
101                }
102            }
103        }
104        return null;
105    }
106
107    public Enumeration<URL> findResources(String resourceName) {
108        synchronized (lock) {
109            return new ResourceEnumeration(new ArrayList<ResourceLocation>(getClassPath().values()), resourceName);
110        }
111    }
112
113    public void addURI(URI uri) {
114        add(Arrays.asList(uri));
115    }
116
117    public URI[] getURIs() {
118        synchronized (lock) {
119            return uris.toArray(new URI[uris.size()]);
120        }
121    }
122
123    /**
124     * Adds a list of uris to the end of this class loader.
125     *
126     * @param uris the URLs to add
127     */
128    protected void add(List<URI> uris) {
129        synchronized (lock) {
130            if (destroyed) {
131                throw new IllegalStateException("UriResourceFinder has been destroyed");
132            }
133            boolean shouldRebuild = this.uris.addAll(uris);
134            if (shouldRebuild) {
135                rebuildClassPath();
136            }
137        }
138    }
139
140    private Map<URI, ResourceLocation> getClassPath() {
141        assert Thread.holdsLock(lock) : "This method can only be called while holding the lock";
142        for (File file : watchedFiles) {
143            if (file.canRead()) {
144                rebuildClassPath();
145                break;
146            }
147        }
148        return classPath;
149    }
150
151    /**
152     * Rebuilds the entire class path. This class is called when new URIs are
153     * added or one of the watched files becomes readable. This method will not
154     * open jar files again, but will add any new entries not alredy open to the
155     * class path. If any file based uri is does not exist, we will watch for
156     * that file to appear.
157     */
158    private void rebuildClassPath() {
159        assert Thread.holdsLock(lock) : "This method can only be called while holding the lock";
160        // copy all of the existing locations into a temp map and clear the class path
161        Map<URI, ResourceLocation> existingJarFiles = new LinkedHashMap<URI, ResourceLocation>(classPath);
162        classPath.clear();
163        LinkedList<URI> locationStack = new LinkedList<URI>(uris);
164        try {
165            while (!locationStack.isEmpty()) {
166                URI uri = locationStack.removeFirst();
167                if (classPath.containsKey(uri)) {
168                    continue;
169                }
170                // Check is this URL has already been opened
171                ResourceLocation resourceLocation = existingJarFiles.remove(uri);
172                // If not opened, cache the uri and wrap it with a resource location
173                if (resourceLocation == null) {
174                    try {
175                        resourceLocation = createResourceLocation(uri.toURL(), cacheUri(uri));
176                    } catch (FileNotFoundException e) {
177                        // if this is a file URL, the file doesn't exist yet... watch to see if it appears later
178                        if ("file".equals(uri.getScheme())) {
179                            File file = new File(uri.getPath());
180                            watchedFiles.add(file);
181                            continue;
182
183                        }
184                    } catch (IOException ignored) {
185                        // can't seem to open the file... this is most likely a bad jar file
186                        // so don't keep a watch out for it because that would require lots of checking
187                        // Dain: We may want to review this decision later
188                        continue;
189                    } catch (UnsupportedOperationException ex) {
190                        // the protocol for the JAR file's URL is not supported.  This can occur when
191                        // the jar file is embedded in an EAR or CAR file.
192                        continue;
193                    }
194                }
195                try {
196                    // add the jar to our class path
197                    if (resourceLocation != null && resourceLocation.getCodeSource() != null) {
198                        classPath.put(resourceLocation.getCodeSource().toURI(), resourceLocation);
199                    }
200                } catch (URISyntaxException ex) {
201                    // ignore
202                }
203                // push the manifest classpath on the stack (make sure to maintain the order)
204                List<URI> manifestClassPath = getManifestClassPath(resourceLocation);
205                locationStack.addAll(0, manifestClassPath);
206            }
207        } catch (Error e) {
208            destroy();
209            throw e;
210        }
211        for (ResourceLocation resourceLocation : existingJarFiles.values()) {
212            resourceLocation.close();
213        }
214    }
215
216    protected File cacheUri(URI uri) throws IOException {
217        if (!"file".equals(uri.getScheme())) {
218            // download the jar
219            throw new UnsupportedOperationException("Only local file jars are supported " + uri);
220        }
221        File file = new File(uri.getPath());
222        if (!file.exists()) {
223            throw new FileNotFoundException(file.getAbsolutePath());
224        }
225        if (!file.canRead()) {
226            throw new IOException("File is not readable: " + file.getAbsolutePath());
227        }
228        return file;
229    }
230
231    protected ResourceLocation createResourceLocation(URL codeSource, File cacheFile) throws IOException {
232        if (!cacheFile.exists()) {
233            throw new FileNotFoundException(cacheFile.getAbsolutePath());
234        }
235        if (!cacheFile.canRead()) {
236            throw new IOException("File is not readable: " + cacheFile.getAbsolutePath());
237        }
238        return cacheFile.isDirectory() ?
239                // DirectoryResourceLocation will only return "file" URLs within this directory
240                // do not use the DirectoryResourceLocation for non file based uris
241                new DirectoryResourceLocation(cacheFile) :
242                new JarResourceLocation(codeSource, cacheFile);
243    }
244
245    private List<URI> getManifestClassPath(ResourceLocation resourceLocation) {
246        List<URI> classPathUrls = new LinkedList<URI>();
247        try {
248            // get the manifest, if possible
249            Manifest manifest = resourceLocation.getManifest();
250            if (manifest == null) {
251                // some locations don't have a manifest
252                return classPathUrls;
253            }
254            // get the class-path attribute, if possible
255            String manifestClassPath = manifest.getMainAttributes().getValue(Attributes.Name.CLASS_PATH);
256            if (manifestClassPath == null) {
257                return classPathUrls;
258            }
259            // build the uris...
260            // the class-path attribute is space delimited
261            URL codeSource = resourceLocation.getCodeSource();
262            for (StringTokenizer tokenizer = new StringTokenizer(manifestClassPath, " "); tokenizer.hasMoreTokens(); ) {
263                String entry = tokenizer.nextToken();
264                try {
265                    // the class path entry is relative to the resource location code source
266                    URL entryUrl = new URL(codeSource, entry);
267                    classPathUrls.add(entryUrl.toURI());
268                } catch (MalformedURLException ignored) {
269                    // most likely a poorly named entry
270                } catch (URISyntaxException ignored) {
271                    // most likely a poorly named entry
272                }
273            }
274            return classPathUrls;
275        } catch (IOException ignored) {
276            // error opening the manifest
277            return classPathUrls;
278        }
279    }
280}