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