// Copyright 2010 Google Inc.
//
// 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 com.google.enterprise.connector.util.database;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A {@link DatabasePropertyResourceBundle} manager which loads and caches
* {@code DatabasePropertyResourceBundles}.
*
* @since 2.8
*/
public class DatabaseResourceBundleManager {
private static final Logger LOGGER =
Logger.getLogger(DatabaseResourceBundleManager.class.getName());
@VisibleForTesting
HashMap<String, DatabasePropertyResourceBundle> cache =
new HashMap<String, DatabasePropertyResourceBundle>();
/**
* Gets a resource bundle using the specified {@code baseName}, and the
* specified {@code resourceBundleExtension}. The {@code baseName}
* identifies the specific set of resources to load, while the
* {@code resourceBundleExtension} is used to locate database-specific
* localizations of those resources.
* <p>
* The passed base name and resource bundle extension are used to
* create resource bundle names. The first name is created by concatenating
* the base name with the resource bundle extension (if provided).
* From this name all parent bundle names are derived. This results in a
* list of possible bundle names.
* <p>
* For example, the {@code baseName "full.package.name.BaseName"}, and the
* {@code resourceBundleExtension "_mysql_5_1"} (for {@code MySQL 5.1}),
* the list of resource bundles to load would look like this:
* <ol>
* <li>full.package.name.BaseName_mysql_5_1.properties</li>
* <li>full.package.name.BaseName_mysql_5.properties</li>
* <li>full.package.name.BaseName_mysql.properties</li>
* <li>full.package.name.BaseName.properties</li>
* </ol>
*
* This list also shows the order in which the bundles will be searched.
* This implementation looks only for Properties file-backed
* {@link DatabasePropertyResourceBundle DatabasePropertyResourceBundles}.
* The properties files used by {@code DatabasePropertyResourceBundle}
* may use the enhanced syntax supported by
* <a href="http://code.google.com/p/eproperties">EProperties</a>
* <p>
* This method tries to load a {@code .properties} file with the names
* by replacing dots in the base name with a slash and by appending
* "{@code .properties}" to the end of the string. If such a resource can be
* found by calling {@link ClassLoader#getResource(String)} it is used to
* initialize a {@link DatabasePropertyResourceBundle}. The resource loader
* will also load all of the ancestors of this resource bundle (as
* described above).
*
* @param baseName the base name of the resource bundle, including the full
* package name
* @param resourceBundleExtension the database-specific localization of the
* resource bundle, or {@code null} if no localized bundle should
* be searched for
* @param classLoader the ClassLoader to use when locating the requested
* resource, or {@code null} to use the default ClassLoader
*
* @return a resource bundle for the given base name and bundle extension, or
* {@code null} if no resource bundle for the specified base name
* can be found.
* @throws NullPointerException if {@code baseName} is {@code null}
*/
public synchronized DatabasePropertyResourceBundle getResourceBundle(
String baseName, String resourceBundleExtension,
ClassLoader classLoader) {
Preconditions.checkNotNull(baseName);
String extension = Strings.nullToEmpty(resourceBundleExtension);
String key = getKey(baseName, extension);
if (!cache.containsKey(key)) {
List<String> bundleNames = getBundleNames(baseName, extension);
loadBundles(bundleNames, getClassLoader(classLoader));
}
return cache.get(key);
}
/**
* Generates a cache lookup key from the supplied {@code baseName} and
* {@code databaseInfo}.
*/
@VisibleForTesting
String getKey(String baseName, String resourceBundleExtension) {
return baseName + Strings.nullToEmpty(resourceBundleExtension);
}
/**
* Returns the {@link ClassLoader} to use.
*/
private ClassLoader getClassLoader(ClassLoader classLoader) {
if (classLoader == null) {
classLoader = this.getClass().getClassLoader();
if (classLoader == null) {
classLoader = ClassLoader.getSystemClassLoader();
}
}
return classLoader;
}
/**
* Builds a list of the bundle names to load, in load order, from
* the supplied {@code baseName} and {@code resourceBundleExtension}.
*/
@VisibleForTesting
List<String> getBundleNames(String baseName, String resourceBundleExtension) {
List<String> names = new ArrayList<String>();
for (int i = 0; i <= resourceBundleExtension.length(); i++) {
i = resourceBundleExtension.indexOf('_', i);
if (i < 0) i = resourceBundleExtension.length();
names.add(baseName + resourceBundleExtension.substring(0, i));
}
return names;
}
/**
* Loads any bundles in the supplied list that are not already loaded,
* caches them, and sets parent-child relationship.
*/
@VisibleForTesting
void loadBundles(List<String> bundleNames, ClassLoader classLoader) {
DatabasePropertyResourceBundle parent = null;
for (String name : bundleNames) {
DatabasePropertyResourceBundle bundle = cache.get(name);
if (bundle == null) {
bundle = loadBundle(name, parent, classLoader);
if (bundle == null) {
bundle = parent;
}
cache.put(name, bundle);
}
parent = bundle;
}
}
/**
* Loads the specified resource bundle.
*
* @param bundleName the name of the resource bundle to load
* @param parent the DatabasePropertyResourceBundle to assign as parent
* @param classLoader the ClassLoader to use to locate the requested resource
*
* @return a DatabasePropertyResourceBundle or {@code null} if none was found
*/
@VisibleForTesting
DatabasePropertyResourceBundle loadBundle(String bundleName,
DatabasePropertyResourceBundle parent, ClassLoader classLoader) {
String resourceName = bundleName.replace('.', '/') + ".properties";
LOGGER.fine("Looking for SQL ResourceBundle " + resourceName);
try {
URL url = classLoader.getResource(resourceName);
if (url != null) {
LOGGER.fine("Loading SQL ResourceBundle " + url);
return new DatabasePropertyResourceBundle(url, parent);
}
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to load SQL ResourceBundle "
+ bundleName, e);
}
return null;
}
}