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