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 - 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            if (bean.contains(".")) {
151                String method = StringHelper.after(bean, ".");
152                bean = StringHelper.before(bean, ".") + "?method=" + method;
153            }
154            Exchange dummy = new DefaultExchange(camelContext);
155            Object out = camelContext.resolveLanguage("bean").createExpression(bean).evaluate(dummy, Object.class);
156            if (dummy.getException() != null) {
157                IOException io = new IOException("Cannot find resource: " + uri + " from calling the bean");
158                io.initCause(dummy.getException());
159                throw io;
160            }
161            if (out != null) {
162                InputStream is = camelContext.getTypeConverter().tryConvertTo(InputStream.class, dummy, out);
163                if (is == null) {
164                    String text = camelContext.getTypeConverter().tryConvertTo(String.class, dummy, out);
165                    if (text != null) {
166                        return new ByteArrayInputStream(text.getBytes());
167                    }
168                } else {
169                    return is;
170                }
171            } else {
172                throw new IOException("Cannot find resource: " + uri + " from calling the bean");
173            }
174        }
175
176        InputStream is = resolveResourceAsInputStream(camelContext.getClassResolver(), uri);
177        if (is == null) {
178            String resolvedName = resolveUriPath(uri);
179            throw new FileNotFoundException("Cannot find resource: " + resolvedName + " in classpath for URI: " + uri);
180        } else {
181            return is;
182        }
183    }
184
185    /**
186     * Resolves the mandatory resource.
187     * <p/>
188     * If possible recommended to use {@link #resolveMandatoryResourceAsUrl(org.apache.camel.spi.ClassResolver, String)}
189     *
190     * @param classResolver the class resolver to load the resource from the classpath
191     * @param uri URI of the resource
192     * @return the resource as an {@link InputStream}.  Remember to close this stream after usage.
193     * @throws java.io.IOException is thrown if the resource file could not be found or loaded as {@link InputStream}
194     * @deprecated use {@link #resolveMandatoryResourceAsInputStream(CamelContext, String)}
195     */
196    @Deprecated
197    public static InputStream resolveMandatoryResourceAsInputStream(ClassResolver classResolver, String uri) throws IOException {
198        InputStream is = resolveResourceAsInputStream(classResolver, uri);
199        if (is == null) {
200            String resolvedName = resolveUriPath(uri);
201            throw new FileNotFoundException("Cannot find resource: " + resolvedName + " in classpath for URI: " + uri);
202        } else {
203            return is;
204        }
205    }
206
207    /**
208     * Resolves the resource.
209     * <p/>
210     * If possible recommended to use {@link #resolveMandatoryResourceAsUrl(org.apache.camel.spi.ClassResolver, String)}
211     *
212     * @param classResolver the class resolver to load the resource from the classpath
213     * @param uri URI of the resource
214     * @return the resource as an {@link InputStream}. Remember to close this stream after usage. Or <tt>null</tt> if not found.
215     * @throws java.io.IOException is thrown if error loading the resource
216     */
217    public static InputStream resolveResourceAsInputStream(ClassResolver classResolver, String uri) throws IOException {
218        if (uri.startsWith("file:")) {
219            uri = ObjectHelper.after(uri, "file:");
220            uri = tryDecodeUri(uri);
221            LOG.trace("Loading resource: {} from file system", uri);
222            return new FileInputStream(uri);
223        } else if (uri.startsWith("http:")) {
224            URL url = new URL(uri);
225            LOG.trace("Loading resource: {} from HTTP", uri);
226            URLConnection con = url.openConnection();
227            con.setUseCaches(false);
228            try {
229                return con.getInputStream();
230            } catch (IOException e) {
231                // close the http connection to avoid
232                // leaking gaps in case of an exception
233                if (con instanceof HttpURLConnection) {
234                    ((HttpURLConnection) con).disconnect();
235                }
236                throw e;
237            }
238        } else if (uri.startsWith("classpath:")) {
239            uri = ObjectHelper.after(uri, "classpath:");
240            uri = tryDecodeUri(uri);
241        } else if (uri.contains(":")) {
242            LOG.trace("Loading resource: {} with UrlHandler for protocol {}", uri, uri.split(":")[0]);
243            URL url = new URL(uri);
244            URLConnection con = url.openConnection();
245            return con.getInputStream();
246        }
247
248        // load from classpath by default
249        String resolvedName = resolveUriPath(uri);
250        LOG.trace("Loading resource: {} from classpath", resolvedName);
251        return classResolver.loadResourceAsStream(resolvedName);
252    }
253
254    /**
255     * Resolves the mandatory resource.
256     *
257     * @param classResolver the class resolver to load the resource from the classpath
258     * @param uri uri of the resource
259     * @return the resource as an {@link java.net.URL}.
260     * @throws java.io.FileNotFoundException is thrown if the resource file could not be found
261     * @throws java.net.MalformedURLException if the URI is malformed
262     */
263    public static URL resolveMandatoryResourceAsUrl(ClassResolver classResolver, String uri) throws FileNotFoundException, MalformedURLException {
264        URL url = resolveResourceAsUrl(classResolver, uri);
265        if (url == null) {
266            String resolvedName = resolveUriPath(uri);
267            throw new FileNotFoundException("Cannot find resource: " + resolvedName + " in classpath for URI: " + uri);
268        } else {
269            return url;
270        }
271    }
272
273    /**
274     * Resolves the resource.
275     *
276     * @param classResolver the class resolver to load the resource from the classpath
277     * @param uri uri of the resource
278     * @return the resource as an {@link java.net.URL}. Or <tt>null</tt> if not found.
279     * @throws java.net.MalformedURLException if the URI is malformed
280     */
281    public static URL resolveResourceAsUrl(ClassResolver classResolver, String uri) throws MalformedURLException {
282        if (uri.startsWith("file:")) {
283            // check if file exists first
284            String name = ObjectHelper.after(uri, "file:");
285            uri = tryDecodeUri(uri);
286            LOG.trace("Loading resource: {} from file system", uri);
287            File file = new File(name);
288            if (!file.exists()) {
289                return null;
290            }
291            return new URL(uri);
292        } else if (uri.startsWith("http:")) {
293            LOG.trace("Loading resource: {} from HTTP", uri);
294            return new URL(uri);
295        } else if (uri.startsWith("classpath:")) {
296            uri = ObjectHelper.after(uri, "classpath:");
297            uri = tryDecodeUri(uri);
298        } else if (uri.contains(":")) {
299            LOG.trace("Loading resource: {} with UrlHandler for protocol {}", uri, uri.split(":")[0]);
300            return new URL(uri);
301        }
302
303        // load from classpath by default
304        String resolvedName = resolveUriPath(uri);
305        LOG.trace("Loading resource: {} from classpath", resolvedName);
306        return classResolver.loadResourceAsURL(resolvedName);
307    }
308
309    /**
310     * Is the given uri a http uri?
311     *
312     * @param uri the uri
313     * @return <tt>true</tt> if the uri starts with <tt>http:</tt> or <tt>https:</tt>
314     */
315    public static boolean isHttpUri(String uri) {
316        if (uri == null) {
317            return false;
318        }
319        return uri.startsWith("http:") || uri.startsWith("https:");
320    }
321
322    /**
323     * Appends the parameters to the given uri
324     *
325     * @param uri the uri
326     * @param parameters the additional parameters (will clear the map)
327     * @return a new uri with the additional parameters appended
328     * @throws URISyntaxException is thrown if the uri is invalid
329     */
330    public static String appendParameters(String uri, Map<String, Object> parameters) throws URISyntaxException {
331        // add additional parameters to the resource uri
332        if (!parameters.isEmpty()) {
333            String query = URISupport.createQueryString(parameters);
334            URI u = new URI(uri);
335            u = URISupport.createURIWithQuery(u, query);
336            parameters.clear();
337            return u.toString();
338        } else {
339            return uri;
340        }
341    }
342
343    /**
344     * Helper operation used to remove relative path notation from
345     * resources.  Most critical for resources on the Classpath
346     * as resource loaders will not resolve the relative paths correctly.
347     *
348     * @param name the name of the resource to load
349     * @return the modified or unmodified string if there were no changes
350     */
351    private static String resolveUriPath(String name) {
352        // compact the path and use / as separator as that's used for loading resources on the classpath
353        return FileUtil.compactPath(name, '/');
354    }
355
356    /**
357     * Tries decoding the uri.
358     *
359     * @param uri the uri
360     * @return the decoded uri, or the original uri
361     */
362    private static String tryDecodeUri(String uri) {
363        try {
364            // try to decode as the uri may contain %20 for spaces etc
365            uri = URLDecoder.decode(uri, "UTF-8");
366        } catch (Exception e) {
367            LOG.trace("Error URL decoding uri using UTF-8 encoding: {}. This exception is ignored.", uri);
368            // ignore
369        }
370        return uri;
371    }
372
373}