/*
* sulky-resources - inheritance-safe class resources.
* Copyright (C) 2002-2016 Joern Huxhorn
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright 2002-2016 Joern Huxhorn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.huxhorn.sulky.resources;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p><code>Resources</code> provides "object-oriented" resource resolution
* and is supposed to be a replacement for <code>getResource(String)</code>
* and <code>getResourceAsStream(String)</code> of <code>java.lang.Class</code>.</p>
*
* DOCUMENT: moep
* Describe ordinary resource-location
* - Using Class.getResource() or Class.getResourceAsStream()
*
* Document reasons for static class
* - because the correct location for methods of this class would be in java.lang.Class
*
* Overall use of this class
* Example/how-to-use-properly
*
* <p><strong>Question Of The Week:</strong> What is the plural of suffix?</p>
* <p><strong>Answer:</strong> There is no answer in any available dictionary. But since
* the latin root of "suffix"
* is the adjective "suffixus" (derived from "subfigere") it is believed that the
* correct plural is "suffixes" and not "suffices" as in "index/indices".</p>
* <p>Therefore "suffixes" is used in this document.</p>
*
* @see ResourceSupport ResourceSupport is a helper class that provides shortcuts for given objects.
* @see Class#getResource(java.lang.String)
* @see Class#getResourceAsStream(java.lang.String)
*/
public final class Resources
{
/**
* Suffix that is used for links. It's value is ".link".
*/
public static final String LINK_SUFFIX = ".link";
private static final String[] EMPTY_STRING_ARRAY = {};
/**
* DOCUMENT: document
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceBaseName the basename off the resource.
* @param suffixes the suffixes to be appended to the basename.
* @param locale the <code>Locale</code> that is used to locate the resource.
* @return an <code>URL</code> array (empty if no resource was found)
* @see #getResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
* @see Class#getResource(java.lang.String)
* @see Class#getResourceAsStream(java.lang.String)
*/
public static URL[] getLocalResources(final Class clazz, final String resourceBaseName, final String[] suffixes, final Locale locale)
{
return getLocalResources(clazz, resourceBaseName, suffixes, locale, false);
}
/**
* This is a shortcut method for <code>getLocalResources(clazz, resourceBaseName, suffixes, null);</code>.
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceBaseName the basename off the resource.
* @param suffixes the suffixes to be appended to the basename.
* @return an <code>URL</code> array (empty if no resource was found)
* @see #getLocalResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static URL[] getLocalResources(final Class clazz, final String resourceBaseName, final String[] suffixes)
{
return getLocalResources(clazz, resourceBaseName, suffixes, null, false);
}
/**
* This is a shortcut method for <code>getLocalResources(clazz, resourceName, {""}, locale);</code>.
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceName the basename off the resource.
* @param locale the <code>Locale</code> that is used to locate the resource.
* @return an <code>URL</code> array (empty if no resource was found)
* @see #getLocalResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static URL[] getLocalResources(final Class clazz, final String resourceName, final Locale locale)
{
return getLocalResources(clazz, resourceName, NO_SUFFIX, locale, false);
}
/**
* This is a shortcut method for <code>getLocalResources(clazz, resourceName, {""}, null);</code>.
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceName the basename off the resource.
* @return an <code>URL</code> array (empty if no resource was found)
* @see #getLocalResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static URL[] getLocalResources(final Class clazz, final String resourceName)
{
return getLocalResources(clazz, resourceName, NO_SUFFIX, null, false);
}
/**
* DOCUMENT: document
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceBaseName the basename off the resource.
* @param suffixes the suffixes to be appended to the basename.
* @param locale the <code>Locale</code> that is used to locate the resource.
* @return an <code>URL</code> to the resource or <code>null</code> if no resource was found.
* @see #getLocalResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static URL getLocalResource(final Class clazz, final String resourceBaseName, final String[] suffixes, final Locale locale)
{
URL[] results = getLocalResources(clazz, resourceBaseName, suffixes, locale, true);
if(results.length == 0)
{
return null;
}
return results[0];
}
/**
* This is a shortcut method for <code>getLocalResource(clazz, resourceBaseName, suffixes, null);</code>.
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceBaseName the basename off the resource.
* @param suffixes the suffixes to be appended to the basename.
* @return an <code>URL</code> to the resource or <code>null</code> if no resource was found.
* @see #getLocalResource(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
* @see #getLocalResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static URL getLocalResource(final Class clazz, final String resourceBaseName, final String[] suffixes)
{
return getLocalResource(clazz, resourceBaseName, suffixes, null);
}
/**
* This is a shortcut method for <code>getLocalResource(clazz, resourceName, {""}, locale);</code>.
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceName the basename off the resource.
* @param locale the <code>Locale</code> that is used to locate the resource.
* @return an <code>URL</code> to the resource or <code>null</code> if no resource was found.
* @see #getLocalResource(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
* @see #getLocalResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static URL getLocalResource(final Class clazz, final String resourceName, final Locale locale)
{
return getLocalResource(clazz, resourceName, NO_SUFFIX, locale);
}
/**
* This is a shortcut method for <code>getLocalResource(clazz, resourceName, {""}, null);</code>.
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceName the basename off the resource.
* @return an <code>URL</code> to the resource or <code>null</code> if no resource was found.
* @see #getLocalResource(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
* @see #getLocalResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static URL getLocalResource(final Class clazz, final String resourceName)
{
return getLocalResource(clazz, resourceName, NO_SUFFIX, null);
}
/**
* DOCUMENT:
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceBaseName the basename off the resource.
* @param suffixes the suffixes to be appended to the basename.
* @param locale the <code>Locale</code> that is used to locate the resource.
* @return an <code>InputStream</code> to the resource or <code>null</code> if no resource was found or opening the <code>InputStream</code> of the resource <code>URL</code> threw an exception.
* @see #getLocalResource(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
* @see #getLocalResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static InputStream getLocalResourceAsStream(final Class clazz, final String resourceBaseName, final String[] suffixes, final Locale locale)
{
return getResourceStream(clazz, resourceBaseName, suffixes, locale, true);
}
/**
* This is a shortcut method for <code>getLocalResourceAsStream(clazz, resourceBaseName, suffixes, null);</code>.
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceBaseName the basename off the resource.
* @param suffixes the suffixes to be appended to the basename.
* @return an <code>InputStream</code> to the resource or <code>null</code> if no resource was found or opening the <code>InputStream</code> of the resource <code>URL</code> threw an exception.
* @see #getLocalResourceAsStream(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
* @see #getLocalResource(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
* @see #getLocalResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static InputStream getLocalResourceAsStream(final Class clazz, final String resourceBaseName, final String[] suffixes)
{
return getLocalResourceAsStream(clazz, resourceBaseName, suffixes, null);
}
/**
* This is a shortcut method for <code>getLocalResourceAsStream(clazz, resourceName, {""}, locale);</code>.
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceName the basename off the resource.
* @param locale the <code>Locale</code> that is used to locate the resource.
* @return an <code>InputStream</code> to the resource or <code>null</code> if no resource was found or opening the <code>InputStream</code> of the resource <code>URL</code> threw an exception.
* @see #getLocalResourceAsStream(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
* @see #getLocalResource(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
* @see #getLocalResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static InputStream getLocalResourceAsStream(final Class clazz, final String resourceName, final Locale locale)
{
return getLocalResourceAsStream(clazz, resourceName, NO_SUFFIX, locale);
}
/**
* This is a shortcut method for <code>getLocalResourceAsStream(clazz, resourceName, {""}, null);</code>.
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceName the basename off the resource.
* @return an <code>InputStream</code> to the resource or <code>null</code> if no resource was found or opening the <code>InputStream</code> of the resource <code>URL</code> threw an exception.
* @see #getLocalResourceAsStream(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
* @see #getLocalResource(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
* @see #getLocalResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static InputStream getLocalResourceAsStream(final Class clazz, final String resourceName)
{
return getLocalResourceAsStream(clazz, resourceName, NO_SUFFIX, null);
}
/**
* DOCUMENT:
* <p>
* This method returns an array of all URL's for the given resourceBaseName,
* suffixes and locale. It will only return valid URLs.
* If no valid URLs exist at all this method returns an empty array.
* </p>
* <p>
* Note that the dot of a suffix needs to be included in the suffixes entries!
* </p>
*
* Example:
* <pre><tt>
* package foobar;
*
* public class Foo
* {
* public static class Bar
* {
* }
* }
* </tt></pre>
* <pre><tt>
* // in some method...
* Class c=foobar.Foo.Bar.class;
* String resourceBaseName="resource";
* String[] suffixes=new String[]{".txt", ".html"};
* Locale.setDefault(new Locale("en_US"));
* Locale locale=new Locale("de_DE");
* </tt></pre>
* <p>
* Result of the call getResources(c, resourceBaseName, suffixes, locale) if ALL files really exist:
* </p>
* <ul>
* <li>/foobar/Foo/Bar/de_DE/resource.txt</li>
* <li>/foobar/Foo/Bar/de_DE/resource.html</li>
* <li>/foobar/Foo/Bar/de/resource.txt</li>
* <li>/foobar/Foo/Bar/de/resource.html</li>
* <li>/foobar/Foo/Bar/en_US/resource.txt</li>
* <li>/foobar/Foo/Bar/en_US/resource.html</li>
* <li>/foobar/Foo/Bar/en/resource.txt</li>
* <li>/foobar/Foo/Bar/en/resource.html</li>
* <li>/foobar/Foo/Bar/resource.txt</li>
* <li>/foobar/Foo/Bar/resource.html</li>
* <li>[same for every parent class of foobar.Foo.Bar]</li>
* <li>[same for declaring class foobar.Foo]</li>
* <li>[same for every parent class of foobar.Foo]</li>
* </ul>
* <p>
* The resources /foobar/resource.txt and /foobar/resource.html are not returned to prevent
* name clashes of resources from different classes in the same package!
* This is a fundamental difference compared to c.getResource(String)/getResourceAsStream(String)
* </p>
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceBaseName the basename off the resource.
* @param suffixes the suffixes to be appended to the basename.
* @param locale the <code>Locale</code> that is used to locate the resource.
* @return an <code>URL</code> array (empty if no resource was found)
* @see #getLocalResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
* @see Class#getResource(java.lang.String)
* @see Class#getResourceAsStream(java.lang.String)
*/
public static URL[] getResources(final Class clazz, final String resourceBaseName, final String[] suffixes, final Locale locale)
{
return getResources(clazz, resourceBaseName, suffixes, locale, false);
}
/**
* This is a shortcut method for <code>getResources(clazz, resourceBaseName, suffixes, null);</code>.
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceBaseName the basename off the resource.
* @param suffixes the suffixes to be appended to the basename.
* @return an <code>URL</code> array (empty if no resource was found)
* @see #getResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static URL[] getResources(final Class clazz, final String resourceBaseName, final String[] suffixes)
{
return getResources(clazz, resourceBaseName, suffixes, null, false);
}
/**
* This is a shortcut method for <code>getResources(clazz, resourceName, {""}, locale);</code>.
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceName the basename off the resource.
* @param locale the <code>Locale</code> that is used to locate the resource.
* @return an <code>URL</code> array (empty if no resource was found)
* @see #getResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static URL[] getResources(final Class clazz, final String resourceName, final Locale locale)
{
return getResources(clazz, resourceName, NO_SUFFIX, locale, false);
}
/**
* This is a shortcut method for <code>getResources(clazz, resourceName, {""}, null);</code>.
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceName the basename off the resource.
* @return an <code>URL</code> array (empty if no resource was found)
* @see #getResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static URL[] getResources(final Class clazz, final String resourceName)
{
return getResources(clazz, resourceName, NO_SUFFIX, null, false);
}
/**
* DOCUMENT: document
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceBaseName the basename off the resource.
* @param suffixes the suffixes to be appended to the basename.
* @param locale the <code>Locale</code> that is used to locate the resource.
* @return an <code>URL</code> to the resource or <code>null</code> if no resource was found.
* @see #getResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static URL getResource(final Class clazz, final String resourceBaseName, final String[] suffixes, final Locale locale)
{
URL[] resourceUrls = getResources(clazz, resourceBaseName, suffixes, locale, true);
if(resourceUrls.length == 0)
{
return null;
}
return resourceUrls[0];
}
/**
* This is a shortcut method for <code>getResource(clazz, resourceBaseName, suffixes, null);</code>.
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceBaseName the basename off the resource.
* @param suffixes the suffixes to be appended to the basename.
* @return an <code>URL</code> to the resource or <code>null</code> if no resource was found.
* @see #getResource(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static URL getResource(final Class clazz, final String resourceBaseName, final String[] suffixes)
{
return getResource(clazz, resourceBaseName, suffixes, null);
}
/**
* This is a shortcut method for <code>getResource(clazz, resourceName, {""}, locale);</code>.
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceName the basename off the resource.
* @param locale the <code>Locale</code> that is used to locate the resource.
* @return an <code>URL</code> to the resource or <code>null</code> if no resource was found.
* @see #getResource(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static URL getResource(final Class clazz, final String resourceName, final Locale locale)
{
return getResource(clazz, resourceName, NO_SUFFIX, locale);
}
/**
* This is a shortcut method for <code>getResource(clazz, resourceName, {""}, null);</code>.
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceName the basename off the resource.
* @return an <code>URL</code> to the resource or <code>null</code> if no resource was found.
* @see #getResource(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static URL getResource(final Class clazz, final String resourceName)
{
return getResource(clazz, resourceName, NO_SUFFIX, null);
}
/**
* DOCUMENT:
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceBaseName the basename off the resource.
* @param suffixes the suffixes to be appended to the basename.
* @param locale the <code>Locale</code> that is used to locate the resource.
* @return an <code>InputStream</code> to the resource or <code>null</code> if no resource was found or opening the <code>InputStream</code> of the resource <code>URL</code> threw an exception.
* @see #getResource(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
* @see #getResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static InputStream getResourceAsStream(final Class clazz, final String resourceBaseName, final String[] suffixes, final Locale locale)
{
return getResourceStream(clazz, resourceBaseName, suffixes, locale, false);
}
/**
* This is a shortcut method for <code>getResourceAsStream(clazz, resourceBaseName, suffixes, null);</code>.
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceBaseName the basename off the resource.
* @param suffixes the suffixes to be appended to the basename.
* @return an <code>InputStream</code> to the resource or <code>null</code> if no resource was found or opening the <code>InputStream</code> of the resource <code>URL</code> threw an exception.
* @see #getResourceAsStream(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
* @see #getResource(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
* @see #getResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static InputStream getResourceAsStream(final Class clazz, final String resourceBaseName, final String[] suffixes)
{
return getResourceAsStream(clazz, resourceBaseName, suffixes, null);
}
/**
* This is a shortcut method for <code>getResourceAsStream(clazz, resourceName, {""}, locale);</code>.
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceName the basename off the resource.
* @param locale the <code>Locale</code> that is used to locate the resource.
* @return an <code>InputStream</code> to the resource or <code>null</code> if no resource was found or opening the <code>InputStream</code> of the resource <code>URL</code> threw an exception.
* @see #getResourceAsStream(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
* @see #getResource(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
* @see #getResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static InputStream getResourceAsStream(final Class clazz, final String resourceName, final Locale locale)
{
return getResourceAsStream(clazz, resourceName, NO_SUFFIX, locale);
}
/**
* This is a shortcut method for <code>getResourceAsStream(clazz, resourceName, {""}, null);</code>.
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceName the basename off the resource.
* @return an <code>InputStream</code> to the resource or <code>null</code> if no resource was found or opening the <code>InputStream</code> of the resource <code>URL</code> threw an exception.
* @see #getResourceAsStream(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
* @see #getResource(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
* @see #getResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
public static InputStream getResourceAsStream(final Class clazz, final String resourceName)
{
return getResourceAsStream(clazz, resourceName, NO_SUFFIX, null);
}
// misc helper-methods below
/**
* Returns the short classname of the given <code>Class</code> without package e.g.
* Bar for class foo.Bar.
*
* @param clazz the <code>Class</code> for which the short classname should be resolved.
* @return the short classname of the given <code>Class</code>
*/
public static String getShortClassName(final Class clazz)
{
String fullName = clazz.getName();
int idx = fullName.lastIndexOf('.');
if(idx == -1)
{
return fullName;
}
idx++;
return fullName.substring(idx);
}
/**
* Returns a <code>String</code> array that contains all <code>Locale</code>
* suffixes for both the given <code>locale</code> and the default <code>Locale</code> in this order.
* The resulting array will not contain duplicate entries.
*
* Examples:
* <dl>
* <dt><code>Locale("de_DE")</code> and default <code>Locale("en_US")</code></dt>
* <dd>returns <code>{"de_DE", "de", "en_US", "en"}</code></dd>
* <dt><code>Locale("de")</code> and default <code>Locale("en_US")</code></dt>
* <dd>returns <code>{"de", "en_US", "en"}</code></dd>
* <dt><code>Locale("en_US")</code> and default <code>Locale("en_US")</code></dt>
* <dd>returns <code>{"en_US", "en"}</code></dd>
* <dt><code>null</code> and default <code>null</code></dt>
* <dd>returns <code>{}</code></dd>
* </dl>
* @param locale the <code>Locale</code> for which a suffix array will be created (beside <code>Locale.getDefault()</code>).
* @return a <code>String</code> array containing all representations
* of the given <code>locale</code> and the default <code>Locale</code> with the most specific first.
* There won't be any duplicates if they overlap.
* @see Locale#getDefault()
* @see Locale#setDefault(java.util.Locale)
*/
public static String[] getLocaleSuffixArray(final Locale locale)
{
String[] localeSuf = getSingleLocaleSuffixArray(locale);
String[] defaultSuf = getSingleLocaleSuffixArray(Locale.getDefault());
List<String> resultList = new ArrayList<>(localeSuf.length + defaultSuf.length);
for(String currentSuffix : localeSuf)
{
if(!resultList.contains(currentSuffix))
{
resultList.add(currentSuffix);
}
}
for(String currentSuffix : defaultSuf)
{
if(!resultList.contains(currentSuffix))
{
resultList.add(currentSuffix);
}
}
String[] result = new String[resultList.size()];
resultList.toArray(result);
return result;
}
/**
* Returns a <code>String</code> array that contains at most
* [language + "_" + country + "_" + variant, language + "_" + country, language] for the given <code>locale</code>.
* Returns an empty array if <code>locale</code> is <code>null</code> or empty.
*
* Examples:
* <dl>
* <dt><code>Locale("de")</code></dt>
* <dd>returns <code>{"de"}</code></dd>
* <dt><code>Locale("de","DE")</code></dt>
* <dd>returns <code>{"de_DE", "de"}</code></dd>
* <dt><code>Locale("de","DE","hessisch")</code></dt>
* <dd>returns <code>{"de_DE_hessisch", "de_DE", "de"}</code></dd>
* <dt><code>Locale("","","foo")</code></dt>
* <dd>returns <code>{"__foo"}</code></dd>
* <dt><code>Locale("foo","","bar")</code></dt>
* <dd>returns <code>{"foo__bar", "foo"}</code></dd>
* <dt><code>Locale("","","")</code> and <code>null</code></dt>
* <dd>return <code>{}</code></dd>
* </dl>
*
* @param locale the <code>Locale</code> for which a suffix array will be created.
* @return a <code>String</code> array containing all representations of the given locale with the most specific first.
*/
public static String[] getSingleLocaleSuffixArray(final Locale locale)
{
if(locale == null)
{
return EMPTY_STRING_ARRAY;
}
final String language = locale.getLanguage();
final int languageLength = language.length();
final String country = locale.getCountry();
final int countryLength = country.length();
final String variant = locale.getVariant();
final int variantLength = variant.length();
List<String> resultList = new ArrayList<>(3);
if(languageLength + countryLength + variantLength != 0)
{
final StringBuilder temp = new StringBuilder();
//temp.append('_');
temp.append(language);
if(languageLength > 0)
{
resultList.add(temp.toString());
}
if(countryLength + variantLength != 0)
{
temp.append('_');
temp.append(country);
if(countryLength > 0)
{
resultList.add(temp.toString());
}
if(variantLength != 0)
{
temp.append('_');
temp.append(variant);
resultList.add(temp.toString());
}
}
}
String[] result = new String[resultList.size()];
Collections.reverse(resultList);
resultList.toArray(result);
return result;
}
/**
* This method returns the path to the class-file of the class without
* the .class-Extension, e.g. for class foo.Bar this method will return
* the String "/foo/Bar".
* Be aware that the internal class foo.Bar$Foobar is mapped to /foo/Bar/Foobar
* instead of /foo/Bar$Foobar!
*
* @param clazz the <code>Class</code> for which the path should be created.
* @return generally the path to the class-file of the class without the .class-extension. Inner classes are handled differently.
*/
public static String getPathToClass(final Class clazz)
{
String className = clazz.getName();
StringBuilder result = new StringBuilder(className.length() + 1);
result.append("/");
className = className.replace('.', '/');
result.append(className.replace('$', '/')); // use subdirs for internal classes instead
return result.toString();
}
/**
* This method returns the path to the package of the given class, e.g. for class
* foo.Bar this method will return the String "/foo".
* Be aware, however, that no trailing separator is added to this path!
*
* @param clazz the <code>Class</code> for which the package path should be resolved.
* @return the path to the package of the class
*/
public static String getPathToPackage(final Class clazz)
{
Package p = clazz.getPackage();
if(p == null)
{
return "/";
}
return "/" + p.getName().replace('.', '/');
}
/**
* Used internally to replace suffixes that are null or zero length.
*/
private static final String[] NO_SUFFIX = new String[]{""};
/**
* private constructor, no instances needed/possible.
*/
private Resources()
{
}
/**
* Reads the contents of the given InputStream and returns a list that contains each
* line of the stream that's neither empty (after stripping whitespaces)
* nor a comment (starting with a #).
*
* @param is the stream to be read into a list of strings.
* @return a List containing all lines that are neither empty nor a comment
* @throws IOException if reading of the stream fails.
*/
private static List<String> readLinkInputStream(InputStream is)
throws IOException
{
final Logger logger = LoggerFactory.getLogger(Resources.class);
List<String> result = new ArrayList<>();
BufferedReader br = null;
IOException exception = null;
try
{
InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
br = new BufferedReader(isr);
String sl;
for(;;)
{
sl = br.readLine();
if(sl == null)
{
break;
}
sl = sl.trim();
if(sl.length() == 0)
{
continue;
}
if(sl.startsWith("#"))
{
if(logger.isDebugEnabled()) logger.debug("Comment: " + sl.substring(1));
continue;
}
result.add(sl);
}
}
catch(IOException ex)
{
exception = ex;
}
finally
{
if(br != null)
{
try
{
br.close();
}
catch(IOException ex)
{
// ignore
}
}
}
if(exception != null)
{
// rethrow after correct close...
throw exception;
}
return result;
}
private static URL resolveLink(final Class clazz, final String resourcePath)
{
List<String> stack = new ArrayList<>();
return recursiveResolve(stack, clazz, resourcePath);
}
private static URL recursiveResolve(List<String> stack, Class clazz, String resourcePath)
{
final Logger logger = LoggerFactory.getLogger(Resources.class);
String resourceLinkPath = resourcePath + LINK_SUFFIX;
URL result = null;
InputStream is = clazz.getResourceAsStream(resourceLinkPath);
if(is != null)
{
// we found a link!
// check for cyclic link and add current resourceLinkPath
// to the linkStack
if(logger.isDebugEnabled()) logger.debug("Found a link '{}' for resource '{}'.", resourceLinkPath, resourcePath);
// this is necessary because Class.getResourceAsStream is case-insensitive
String lowLinkPath = resourceLinkPath.toLowerCase(Locale.US);
if(stack.contains(lowLinkPath))
{
// the exception is only used for logging (stack-trace) and won't be thrown...
if(logger.isWarnEnabled())
{
//noinspection ThrowableInstanceNeverThrown
logger.warn("Found a cyclic link!", new CyclicLinkException(stack, resourceLinkPath));
}
return null;
}
stack.add(lowLinkPath);
List<String> linkContent;
try
{
// read and parse the linkContent...
linkContent = readLinkInputStream(is);
Iterator iter = linkContent.iterator();
while(result == null && iter.hasNext())
{
String currentLinkTarget = (String) iter.next();
// empty lines are allready ignored in readLinkInputStream
// Stack is cloned to support multiple lines...
String basePath = PathTools.getParentPath(resourceLinkPath);
String previousLinkTarget=currentLinkTarget;
currentLinkTarget = PathTools.getAbsolutePath(basePath, previousLinkTarget);
if(currentLinkTarget == null)
{
if(logger.isDebugEnabled())
{
logger
.debug("getAbsolutePath(\"" + basePath + "\", \"" + previousLinkTarget + "\") returned null - no valid absolute path found.");
}
}
else
{
if(logger.isDebugEnabled()) logger.debug("Checking for link-target '{}'.", currentLinkTarget);
result = recursiveResolve(new ArrayList<>(stack), clazz, currentLinkTarget);
if(result != null)
{
if(logger.isDebugEnabled()) logger.debug("Found link-target '{}'.", currentLinkTarget);
}
else
{
if(logger.isDebugEnabled()) logger.debug("Found unsatisfied link '{}'.", currentLinkTarget);
}
}
}
}
catch(IOException ex)
{
if(logger.isWarnEnabled()) logger.warn("Exception while reading link-content of '{}'.", resourceLinkPath, ex);
}
if(result != null)
{
// if we found a result return it...
return result;
}
}// end if link
// ... otherwise simply return the original resource-stream if available.
return clazz.getResource(resourcePath);
}
/**
* Used by the private getLocalResources and getResources methods to collect the resources.
*
* @param urlList collected resources are put into this list.
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceBaseName the basename off the resource.
* @param suffixes the suffixes to be appended to the basename.
* @param locale the <code>Locale</code> that is used to locate the resource.
* @param firstOnly if <code>true</code>, this method will stop searching after a single result has been found, returning immediatly.
* @see #getLocalResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale, boolean)
* @see #getResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale, boolean)
*/
private static void collectResources(List<URL> urlList, final Class clazz, final String resourceBaseName, final String[] suffixes, final Locale locale, final boolean firstOnly)
{
final Logger logger = LoggerFactory.getLogger(Resources.class);
Class currentClass = clazz;
while(currentClass != java.lang.Object.class)
{
if(logger.isDebugEnabled()) logger.debug("currentClass = " + currentClass.getName());
URL[] urls = getLocalResources(currentClass, resourceBaseName, suffixes, locale, firstOnly);
for(int i = 0; i < urls.length; i++)
{
if(!urlList.contains(urls[i]))
{
urlList.add(urls[i]);
if(logger.isDebugEnabled()) logger.debug("added url[{}]: {}", i, urls[i]);
if(firstOnly)
{
return;
}
}
}
currentClass = currentClass.getSuperclass();
}
}
/**
* private method that is used by the public getLocalResource/getLocalResources methods.
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceBaseName the basename off the resource.
* @param suffixes the suffixes to be appended to the basename.
* @param locale the <code>Locale</code> that is used to locate the resource.
* @param firstOnly if <code>true</code>, this method will stop searching after a single result has been found, returning immediatly.
* @return an <code>URL</code> array (empty if no resource was found)
* @see #getLocalResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
* @see #getLocalResource(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
private static URL[] getLocalResources(final Class clazz, final String resourceBaseName, final String[] suffixes, final Locale locale, final boolean firstOnly)
{
final Logger logger = LoggerFactory.getLogger(Resources.class);
String basePath = getPathToClass(clazz);
String[] suff;
if(suffixes == null || suffixes.length == 0)
{
suff = NO_SUFFIX;
}
else
{
suff = suffixes;
}
List<URL> urls = new ArrayList<>();
// handle locale parameter localePaths
String[] localePaths = getLocaleSuffixArray(locale);
for(String currentLocPath : localePaths)
{
for(String aSuff : suff)
{
String resourceName = resourceBaseName + aSuff;
String currentBase = basePath;
if(currentLocPath.length() != 0)
{
currentBase = currentBase + "/" + currentLocPath;
}
String currentPath = PathTools.getAbsolutePath(currentBase, resourceName);
if(currentPath == null)
{
if(logger.isDebugEnabled()) logger.debug("getAbsolutePath(\"{}\", \"{}\") returned null - no valid absolute path found.", currentBase, resourceName);
}
else
{
if(logger.isDebugEnabled()) logger.debug("Trying to obtain URL for resource '{}'.", currentPath);
URL url = resolveLink(clazz, currentPath);
if(url != null && !urls.contains(url))
{
if(firstOnly)
{
return new URL[]{url};
}
urls.add(url);
if(logger.isDebugEnabled()) logger.debug("Obtained new URL \"{}\" for resource '{}'.", url, currentPath);
}
}
}
}
// check for resource in Class-folder without suffix
for(String aSuff : suff)
{
String resourceName = resourceBaseName + aSuff;
String absResourcePath = PathTools.getAbsolutePath(basePath, resourceName);
if(absResourcePath == null)
{
if(logger.isDebugEnabled()) logger.debug("getAbsolutePath(\"{}\", \"{}\") returned null - no valid absolute path found.", basePath, resourceName);
}
else
{
if(logger.isDebugEnabled()) logger.debug("Trying to obtain URL for resource '{}'.", absResourcePath);
URL url = resolveLink(clazz, absResourcePath);
if(url != null && !urls.contains(url))
{
if(firstOnly)
{
return new URL[]{url};
}
urls.add(url);
if(logger.isDebugEnabled()) logger.debug("Obtained new URL \"{}\" for resource '{}'.", url, absResourcePath);
}
}
}
URL[] result = new URL[urls.size()];
urls.toArray(result);
return result;
}
/**
* private method that is used by the public getResource/getResources methods.
*
* @param clazz the <code>Class</code> that is used to locate the resource.
* @param resourceBaseName the basename off the resource.
* @param suffixes the suffixes to be appended to the basename.
* @param locale the <code>Locale</code> that is used to locate the resource.
* @param firstOnly if <code>true</code>, this method will stop searching after a single result has been found, returning immediatly.
* @return an <code>URL</code> array (empty if no resource was found)
* @see #getResources(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
* @see #getResource(java.lang.Class, java.lang.String, java.lang.String[], java.util.Locale)
*/
private static URL[] getResources(final Class clazz, final String resourceBaseName, final String[] suffixes, final Locale locale, final boolean firstOnly)
{
final Logger logger = LoggerFactory.getLogger(Resources.class);
List<URL> urlList = new ArrayList<>();
// collect resources from given class...
collectResources(urlList, clazz, resourceBaseName, suffixes, locale, firstOnly);
if(!firstOnly || urlList.size() == 0)
{
// collect resources from declaring classes...
Class currentClass = clazz.getDeclaringClass();
while(currentClass != null)
{
collectResources(urlList, currentClass, resourceBaseName, suffixes, locale, firstOnly);
if(firstOnly && urlList.size() != 0)
{
break;
}
currentClass = currentClass.getDeclaringClass();
}
}
URL[] result = new URL[urlList.size()];
urlList.toArray(result);
if(logger.isInfoEnabled() && result.length == 0)
{
StringBuilder msg = new StringBuilder();
msg.append("Couldn't obtain any URL's for resource '").append(resourceBaseName).append("'");
if(suffixes != null && suffixes.length != 0)
{
if(suffixes.length == 1 && suffixes[0].length() == 0)
{
msg.append(" with no suffix. ");
}
else
{
msg.append(" with suffix(es) [");
for(int i = 0; i < suffixes.length; i++)
{
if(i != 0)
{
msg.append(",");
}
msg.append("\"").append(suffixes[i]).append("\"");
}
msg.append("]. ");
}
}
msg.append("Search started at '").append(clazz).append("'.");
logger.info(msg.toString());
}
return result;
}
private static InputStream getResourceStream(final Class clazz, final String resourceBaseName, final String[] suffixes, final Locale locale, final boolean local)
{
InputStream result = null;
URL resource;
if(local)
{
resource = getLocalResource(clazz, resourceBaseName, suffixes, locale);
}
else
{
resource = getResource(clazz, resourceBaseName, suffixes, locale);
}
if(resource != null)
{
try
{
result = resource.openStream();
}
catch(IOException ex)
{
final Logger logger = LoggerFactory.getLogger(Resources.class);
if(logger.isWarnEnabled()) logger.warn("IOException while opening URL-Connection for URL '{}'!", resource, ex);
}
}
return result;
}
}