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.camel.util;
018
019import java.io.ByteArrayInputStream;
020import java.io.File;
021import java.io.FileInputStream;
022import java.io.FileNotFoundException;
023import java.io.IOException;
024import java.io.InputStream;
025import java.net.HttpURLConnection;
026import java.net.MalformedURLException;
027import java.net.URI;
028import java.net.URISyntaxException;
029import java.net.URL;
030import java.net.URLConnection;
031import java.net.URLDecoder;
032import java.util.Map;
033
034import org.apache.camel.CamelContext;
035import org.apache.camel.Exchange;
036import org.apache.camel.RuntimeCamelException;
037import org.apache.camel.impl.DefaultExchange;
038import org.apache.camel.spi.ClassResolver;
039import org.slf4j.Logger;
040import org.slf4j.LoggerFactory;
041
042/**
043 * Helper class for loading resources on the classpath or file system.
044 */
045public final class ResourceHelper {
046
047    private static final Logger LOG = LoggerFactory.getLogger(ResourceHelper.class);
048
049    private ResourceHelper() {
050        // utility class
051    }
052
053    /**
054     * Resolves the expression/predicate whether it refers to an external script on the file/classpath etc.
055     * This requires to use the prefix <tt>resource:</tt> such as <tt>resource:classpath:com/foo/myscript.groovy</tt>,
056     * <tt>resource:file:/var/myscript.groovy</tt>.
057     * <p/>
058     * If not then the returned value is returned as-is.
059     */
060    public static String resolveOptionalExternalScript(CamelContext camelContext, String expression) {
061        if (expression == null) {
062            return null;
063        }
064        String external = expression;
065
066        // must be one line only
067        int newLines = StringHelper.countChar(expression, '\n');
068        if (newLines > 1) {
069            // okay then just use as-is
070            return expression;
071        }
072
073        // must start with resource: to denote an external resource
074        if (external.startsWith("resource:")) {
075            external = external.substring(9);
076
077            if (hasScheme(external)) {
078                InputStream is = null;
079                try {
080                    is = resolveMandatoryResourceAsInputStream(camelContext, external);
081                    expression = camelContext.getTypeConverter().convertTo(String.class, is);
082                } catch (IOException e) {
083                    throw new RuntimeCamelException("Cannot load resource " + external, e);
084                } finally {
085                    IOHelper.close(is);
086                }
087            }
088        }
089
090        return expression;
091    }
092
093    /**
094     * Determines whether the URI has a scheme (e.g. file:, classpath: or http:)
095     *
096     * @param uri the URI
097     * @return <tt>true</tt> if the URI starts with a scheme
098     */
099    public static boolean hasScheme(String uri) {
100        if (uri == null) {
101            return false;
102        }
103
104        return uri.startsWith("file:") || uri.startsWith("classpath:") || uri.startsWith("http:");
105    }
106
107    /**
108     * Gets the scheme from the URI (e.g. file:, classpath: or http:)
109     *
110     * @param uri  the uri
111     * @return the scheme, or <tt>null</tt> if no scheme
112     */
113    public static String getScheme(String uri) {
114        if (hasScheme(uri)) {
115            return uri.substring(0, uri.indexOf(":") + 1);
116        } else {
117            return null;
118        }
119    }
120
121    /**
122     * Resolves the mandatory resource.
123     * <p/>
124     * The resource uri can refer to the following systems to be loaded from
125     * <ul>
126     *     <il>file:nameOfFile - to refer to the file system</il>
127     *     <il>classpath:nameOfFile - to refer to the classpath (default)</il>
128     *     <il>http:uri - to load the resource using HTTP</il>
129     *     <il>ref:nameOfBean - to lookup the resource in the {@link org.apache.camel.spi.Registry}</il>
130     *     <il>bean:nameOfBean.methodName or bean:nameOfBean::methodName - to lookup a bean in the {@link org.apache.camel.spi.Registry} and call the method</il>
131     *     <il><customProtocol>:uri - to lookup the resource using a custom {@link java.net.URLStreamHandler} registered for the <customProtocol>,
132     *     on how to register it @see java.net.URL#URL(java.lang.String, java.lang.String, int, java.lang.String)</il>
133     * </ul>
134     * If no prefix has been given, then the resource is loaded from the classpath
135     * <p/>
136     * If possible recommended to use {@link #resolveMandatoryResourceAsUrl(org.apache.camel.spi.ClassResolver, String)}
137     *
138     * @param camelContext the Camel Context
139     * @param uri URI of the resource
140     * @return the resource as an {@link InputStream}.  Remember to close this stream after usage.
141     * @throws java.io.IOException is thrown if the resource file could not be found or loaded as {@link InputStream}
142     */
143    public static InputStream resolveMandatoryResourceAsInputStream(CamelContext camelContext, String uri) throws IOException {
144        if (uri.startsWith("ref:")) {
145            String ref = uri.substring(4);
146            String value = CamelContextHelper.mandatoryLookup(camelContext, ref, String.class);
147            return new ByteArrayInputStream(value.getBytes());
148        } else if (uri.startsWith("bean:")) {
149            String bean = uri.substring(5);
150            Exchange dummy = new DefaultExchange(camelContext);
151            Object out = camelContext.resolveLanguage("bean").createExpression(bean).evaluate(dummy, Object.class);
152            if (dummy.getException() != null) {
153                IOException io = new IOException("Cannot find resource: " + uri + " from calling the bean");
154                io.initCause(dummy.getException());
155                throw io;
156            }
157            if (out != null) {
158                InputStream is = camelContext.getTypeConverter().tryConvertTo(InputStream.class, dummy, out);
159                if (is == null) {
160                    String text = camelContext.getTypeConverter().tryConvertTo(String.class, dummy, out);
161                    if (text != null) {
162                        return new ByteArrayInputStream(text.getBytes());
163                    }
164                } else {
165                    return is;
166                }
167            } else {
168                throw new IOException("Cannot find resource: " + uri + " from calling the bean");
169            }
170        }
171
172        InputStream is = resolveResourceAsInputStream(camelContext.getClassResolver(), uri);
173        if (is == null) {
174            String resolvedName = resolveUriPath(uri);
175            throw new FileNotFoundException("Cannot find resource: " + resolvedName + " in classpath for URI: " + uri);
176        } else {
177            return is;
178        }
179    }
180
181    /**
182     * Resolves the mandatory resource.
183     * <p/>
184     * If possible recommended to use {@link #resolveMandatoryResourceAsUrl(org.apache.camel.spi.ClassResolver, String)}
185     *
186     * @param classResolver the class resolver to load the resource from the classpath
187     * @param uri URI of the resource
188     * @return the resource as an {@link InputStream}.  Remember to close this stream after usage.
189     * @throws java.io.IOException is thrown if the resource file could not be found or loaded as {@link InputStream}
190     * @deprecated use {@link #resolveMandatoryResourceAsInputStream(CamelContext, String)}
191     */
192    @Deprecated
193    public static InputStream resolveMandatoryResourceAsInputStream(ClassResolver classResolver, String uri) throws IOException {
194        InputStream is = resolveResourceAsInputStream(classResolver, uri);
195        if (is == null) {
196            String resolvedName = resolveUriPath(uri);
197            throw new FileNotFoundException("Cannot find resource: " + resolvedName + " in classpath for URI: " + uri);
198        } else {
199            return is;
200        }
201    }
202
203    /**
204     * Resolves the resource.
205     * <p/>
206     * If possible recommended to use {@link #resolveMandatoryResourceAsUrl(org.apache.camel.spi.ClassResolver, String)}
207     *
208     * @param classResolver the class resolver to load the resource from the classpath
209     * @param uri URI of the resource
210     * @return the resource as an {@link InputStream}. Remember to close this stream after usage. Or <tt>null</tt> if not found.
211     * @throws java.io.IOException is thrown if error loading the resource
212     */
213    public static InputStream resolveResourceAsInputStream(ClassResolver classResolver, String uri) throws IOException {
214        if (uri.startsWith("file:")) {
215            uri = ObjectHelper.after(uri, "file:");
216            uri = tryDecodeUri(uri);
217            LOG.trace("Loading resource: {} from file system", uri);
218            return new FileInputStream(uri);
219        } else if (uri.startsWith("http:")) {
220            URL url = new URL(uri);
221            LOG.trace("Loading resource: {} from HTTP", uri);
222            URLConnection con = url.openConnection();
223            con.setUseCaches(false);
224            try {
225                return con.getInputStream();
226            } catch (IOException e) {
227                // close the http connection to avoid
228                // leaking gaps in case of an exception
229                if (con instanceof HttpURLConnection) {
230                    ((HttpURLConnection) con).disconnect();
231                }
232                throw e;
233            }
234        } else if (uri.startsWith("classpath:")) {
235            uri = ObjectHelper.after(uri, "classpath:");
236            uri = tryDecodeUri(uri);
237        } else if (uri.contains(":")) {
238            LOG.trace("Loading resource: {} with UrlHandler for protocol {}", uri, uri.split(":")[0]);
239            URL url = new URL(uri);
240            URLConnection con = url.openConnection();
241            return con.getInputStream();
242        }
243
244        // load from classpath by default
245        String resolvedName = resolveUriPath(uri);
246        LOG.trace("Loading resource: {} from classpath", resolvedName);
247        return classResolver.loadResourceAsStream(resolvedName);
248    }
249
250    /**
251     * Resolves the mandatory resource.
252     *
253     * @param classResolver the class resolver to load the resource from the classpath
254     * @param uri uri of the resource
255     * @return the resource as an {@link java.net.URL}.
256     * @throws java.io.FileNotFoundException is thrown if the resource file could not be found
257     * @throws java.net.MalformedURLException if the URI is malformed
258     */
259    public static URL resolveMandatoryResourceAsUrl(ClassResolver classResolver, String uri) throws FileNotFoundException, MalformedURLException {
260        URL url = resolveResourceAsUrl(classResolver, uri);
261        if (url == null) {
262            String resolvedName = resolveUriPath(uri);
263            throw new FileNotFoundException("Cannot find resource: " + resolvedName + " in classpath for URI: " + uri);
264        } else {
265            return url;
266        }
267    }
268
269    /**
270     * Resolves the resource.
271     *
272     * @param classResolver the class resolver to load the resource from the classpath
273     * @param uri uri of the resource
274     * @return the resource as an {@link java.net.URL}. Or <tt>null</tt> if not found.
275     * @throws java.net.MalformedURLException if the URI is malformed
276     */
277    public static URL resolveResourceAsUrl(ClassResolver classResolver, String uri) throws MalformedURLException {
278        if (uri.startsWith("file:")) {
279            // check if file exists first
280            String name = ObjectHelper.after(uri, "file:");
281            uri = tryDecodeUri(uri);
282            LOG.trace("Loading resource: {} from file system", uri);
283            File file = new File(name);
284            if (!file.exists()) {
285                return null;
286            }
287            return new URL(uri);
288        } else if (uri.startsWith("http:")) {
289            LOG.trace("Loading resource: {} from HTTP", uri);
290            return new URL(uri);
291        } else if (uri.startsWith("classpath:")) {
292            uri = ObjectHelper.after(uri, "classpath:");
293            uri = tryDecodeUri(uri);
294        } else if (uri.contains(":")) {
295            LOG.trace("Loading resource: {} with UrlHandler for protocol {}", uri, uri.split(":")[0]);
296            return new URL(uri);
297        }
298
299        // load from classpath by default
300        String resolvedName = resolveUriPath(uri);
301        LOG.trace("Loading resource: {} from classpath", resolvedName);
302        return classResolver.loadResourceAsURL(resolvedName);
303    }
304
305    /**
306     * Is the given uri a http uri?
307     *
308     * @param uri the uri
309     * @return <tt>true</tt> if the uri starts with <tt>http:</tt> or <tt>https:</tt>
310     */
311    public static boolean isHttpUri(String uri) {
312        if (uri == null) {
313            return false;
314        }
315        return uri.startsWith("http:") || uri.startsWith("https:");
316    }
317
318    /**
319     * Appends the parameters to the given uri
320     *
321     * @param uri the uri
322     * @param parameters the additional parameters (will clear the map)
323     * @return a new uri with the additional parameters appended
324     * @throws URISyntaxException is thrown if the uri is invalid
325     */
326    public static String appendParameters(String uri, Map<String, Object> parameters) throws URISyntaxException {
327        // add additional parameters to the resource uri
328        if (!parameters.isEmpty()) {
329            String query = URISupport.createQueryString(parameters);
330            URI u = new URI(uri);
331            u = URISupport.createURIWithQuery(u, query);
332            parameters.clear();
333            return u.toString();
334        } else {
335            return uri;
336        }
337    }
338
339    /**
340     * Helper operation used to remove relative path notation from
341     * resources.  Most critical for resources on the Classpath
342     * as resource loaders will not resolve the relative paths correctly.
343     *
344     * @param name the name of the resource to load
345     * @return the modified or unmodified string if there were no changes
346     */
347    private static String resolveUriPath(String name) {
348        // compact the path and use / as separator as that's used for loading resources on the classpath
349        return FileUtil.compactPath(name, '/');
350    }
351
352    /**
353     * Tries decoding the uri.
354     *
355     * @param uri the uri
356     * @return the decoded uri, or the original uri
357     */
358    private static String tryDecodeUri(String uri) {
359        try {
360            // try to decode as the uri may contain %20 for spaces etc
361            uri = URLDecoder.decode(uri, "UTF-8");
362        } catch (Exception e) {
363            LOG.trace("Error URL decoding uri using UTF-8 encoding: {}. This exception is ignored.", uri);
364            // ignore
365        }
366        return uri;
367    }
368
369}