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