org.joda.convert.RenameHandler Maven / Gradle / Ivy
Show all versions of joda-convert Show documentation
/*
* Copyright 2010-present Stephen Colebourne
*
* 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 org.joda.convert;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* A general purpose utility for registering renames.
*
* This handles type and enum constant renames.
* For example, use as follows:
*
* RenameHandler.INSTANCE.renamedType("org.joda.OldName", NewName.class);
* RenameHandler.INSTANCE.renamedEnum("CORRECT", Status.VALID);
* RenameHandler.INSTANCE.renamedEnum("INCORRECT", Status.INVALID);
*
* From v2.1, renames can be stored on the classpath in configuration files.
* The file location is {@code META-INF/org/joda/convert/Renamed.ini}.
* All files found in this location are read and processed.
* The format has two sections {@code [types]} and {@code [enums]}.
* The {@code [types]} section has lines of the format {@code oldClassName = newClassName}.
* The {@code [enums]} section has lines of the format {@code oldEnumConstantName = enumClassName.newEnumConstantName}.
* Lines starting with {@code #} are treated as comments.
*
* The recommended usage is to edit the static singleton before using other classes.
* Editing a static is acceptable because renames are driven by bytecode which is static.
* For additional security, an application should lock the rename handler instance
* once any types and enums have been registered using {@link #lock()}.
*
* This class is thread-safe with concurrent caches.
*
* @since 1.6
*/
public final class RenameHandler {
// NOTE!
// This class must be loaded after StringConvert to avoid horrid loops in class initialization
/**
* Errors in class initialization are hard to debug.
* Set -Dorg.joda.convert.debug=true on the command line to add extra logging to System.err
*
* NOTE! This also forces {@link StringConvert} to be loaded before calling {@link #createInstance()}
* which is vital to avoid horrid ordering issues when loading Renamed.ini classes that
* reference {@code StringConvert}.
*/
private static final boolean LOG = StringConvert.LOG;
/**
* A mutable global instance.
* This is a singleton instance which is mutated.
* It will be populated by the contents of the {@code Renamed.ini} configuration files.
*/
public static final RenameHandler INSTANCE = createInstance();
// this is a method to aid IDE debugging of class initialization
private static RenameHandler createInstance() {
// log errors to System.err, as problems in static initializers can be troublesome to diagnose
var instance = create(false);
try {
// calling loadFromClasspath() is the best option even though it mutates INSTANCE
// only serious errors will be caught here, most errors will log from parseRenameFile()
instance.loadFromClasspath();
} catch (IllegalStateException ex) {
System.err.println("ERROR: " + ex.getMessage());
ex.printStackTrace();
} catch (Throwable ex) {
System.err.println("ERROR: Failed to load Renamed.ini files: " + ex.getMessage());
ex.printStackTrace();
}
return instance;
}
/**
* The lock flag.
*/
private volatile boolean locked;
/**
* The type renames.
*/
private final ConcurrentHashMap> typeRenames = new ConcurrentHashMap<>(16, 0.75f, 2);
/**
* The enum renames.
*/
private final ConcurrentHashMap, Map>> enumRenames = new ConcurrentHashMap<>(16, 0.75f, 2);
//-----------------------------------------------------------------------
/**
* Creates an instance.
*
* This is not normally used as the preferred option is to edit the singleton.
*
* @return a new instance, not null
*/
public static RenameHandler create() {
return new RenameHandler();
}
/**
* Creates an instance, providing the ability to load types in config files.
*
* This is not normally used as the preferred option is to edit the singleton.
*
* If the flag is set to true, the classpath config files will be used to register types and enums.
*
* @param loadFromClasspath whether to load any types in classpath config files
* @return a new instance, not null
*/
public static RenameHandler create(boolean loadFromClasspath) {
var handler = new RenameHandler();
if (loadFromClasspath) {
handler.loadFromClasspath();
}
return handler;
}
//-----------------------------------------------------------------------
/**
* Restricted constructor.
*/
private RenameHandler() {
}
//-----------------------------------------------------------------------
/**
* Register the fact that a type was renamed.
*
* This handles the use case where a class is renamed.
*
* @param oldName the old name of the type including the package name, not null
* @param currentValue the current type, not null
*/
public void renamedType(String oldName, Class> currentValue) {
if (oldName == null) {
throw new IllegalArgumentException("oldName must not be null");
}
if (currentValue == null) {
throw new IllegalArgumentException("currentValue must not be null");
}
if (oldName.startsWith("java.") || oldName.startsWith("javax.") || oldName.startsWith("org.joda.")) {
throw new IllegalArgumentException("oldName must not be a java.*, javax.* or org.joda.* type");
}
checkNotLocked();
typeRenames.put(oldName, currentValue);
}
/**
* Gets the map of renamed types.
*
* An empty map is returned if there are no renames.
*
* @return a copy of the set of enum types with renames, not null
*/
public Map> getTypeRenames() {
return new HashMap<>(typeRenames);
}
/**
* Lookup a type from a name, handling renames.
*
* @param name the name of the type to lookup, not null
* @return the type, not null
* @throws ClassNotFoundException if the name is not a valid type
*/
public Class> lookupType(String name) throws ClassNotFoundException {
if (name == null) {
throw new IllegalArgumentException("name must not be null");
}
var type = typeRenames.get(name);
if (type == null) {
type = StringConvert.loadType(name);
}
return type;
}
//-----------------------------------------------------------------------
/**
* Register the fact that an enum constant was renamed.
*
* This handles the use case where an enum constant is renamed, but the enum class remains the same.
*
* @param oldName the old name of the enum constant, not null
* @param currentValue the current enum constant, not null
*/
public void renamedEnum(String oldName, Enum> currentValue) {
if (oldName == null) {
throw new IllegalArgumentException("oldName must not be null");
}
if (currentValue == null) {
throw new IllegalArgumentException("currentValue must not be null");
}
checkNotLocked();
var enumType = currentValue.getDeclaringClass();
var perClass = enumRenames.get(enumType);
if (perClass == null) {
enumRenames.putIfAbsent(enumType, new ConcurrentHashMap<>(16, 0.75f, 2));
perClass = enumRenames.get(enumType);
}
perClass.put(oldName, currentValue);
}
/**
* Gets the set of enum types that have renames.
*
* An empty set is returned if there are no renames.
*
* @return a copy of the set of enum types with renames, not null
*/
public Set> getEnumTypesWithRenames() {
return new HashSet<>(enumRenames.keySet());
}
/**
* Gets the map of renamed for an enum type.
*
* An empty map is returned if there are no renames.
*
* @param type the enum type, not null
* @return a copy of the set of enum renames, not null
*/
public Map> getEnumRenames(Class> type) {
if (type == null) {
throw new IllegalArgumentException("type must not be null");
}
var map = enumRenames.get(type);
if (map == null) {
return new HashMap<>();
}
return new HashMap<>(map);
}
/**
* Lookup an enum from a name, handling renames.
*
* @param the type of the desired enum
* @param type the enum type, not null
* @param name the name of the enum to lookup, not null
* @return the enum value, not null
* @throws IllegalArgumentException if the name is not a valid enum constant
*/
public > T lookupEnum(Class type, String name) {
if (type == null) {
throw new IllegalArgumentException("type must not be null");
}
if (name == null) {
throw new IllegalArgumentException("name must not be null");
}
var map = getEnumRenames(type);
var value = map.get(name);
if (value != null) {
return type.cast(value);
}
return Enum.valueOf(type, name);
}
//-----------------------------------------------------------------------
/**
* Locks this instance of the rename handler.
*
* For additional security, an application should lock the rename handler
* once any types and enums have been registered.
*/
public void lock() {
checkNotLocked();
locked = true;
}
// ensure not locked
private void checkNotLocked() {
if (locked) {
throw new IllegalStateException("RenameHandler has been locked and it cannot now be changed");
}
}
//-----------------------------------------------------------------------
// loads config files
private void loadFromClasspath() {
URL url = null;
try {
// this is the new location of the file, working on Java 8, Java 9 class path and Java 9 module path
var loader = Thread.currentThread().getContextClassLoader();
if (loader == null) {
loader = RenameHandler.class.getClassLoader();
}
if (LOG) {
System.err.println("Loading from classpath: " + loader);
}
var en = loader.getResources("META-INF/org/joda/convert/Renamed.ini");
while (en.hasMoreElements()) {
url = en.nextElement();
if (LOG) {
System.err.println("Loading file: " + url);
}
var lines = loadRenameFile(url);
parseRenameFile(lines, url);
}
} catch (Exception ex) {
if (LOG) {
ex.printStackTrace(System.err);
}
throw new IllegalStateException("Unable to load Renamed.ini: " + url + ": " + ex.getMessage(), ex);
}
}
// loads a single rename file
private List loadRenameFile(URL url) throws IOException {
var lines = new ArrayList();
try (var reader = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
var trimmed = line.trim();
if (!trimmed.isEmpty() && !trimmed.startsWith("#")) {
lines.add(trimmed);
}
}
}
return lines;
}
// parses a single rename file
private void parseRenameFile(List lines, URL url) {
// format allows multiple [types] and [enums] so file can be merged
var types = false;
var enums = false;
for (var line : lines) {
try {
if (line.equals("[types]")) {
types = true;
enums = false;
} else if (line.equals("[enums]")) {
types = false;
enums = true;
} else if (types) {
var equalsPos = line.indexOf('=');
if (equalsPos < 0) {
throw new IllegalArgumentException(
"Renamed.ini type line must be formatted as 'oldClassName = newClassName'");
}
var oldName = line.substring(0, equalsPos).trim();
var newName = line.substring(equalsPos + 1).trim();
Class> newClass = null;
try {
newClass = StringConvert.loadType(newName);
} catch (Throwable ex) {
if (LOG) {
ex.printStackTrace(System.err);
}
throw new IllegalArgumentException(
"Class.forName(" + newName + ") failed: " + ex.getMessage());
}
renamedType(oldName, newClass);
} else if (enums) {
var equalsPos = line.indexOf('=');
var lastDotPos = line.lastIndexOf('.');
if (equalsPos < 0 || lastDotPos < 0 || lastDotPos < equalsPos) {
throw new IllegalArgumentException(
"Renamed.ini enum line must be formatted as 'oldEnumConstantName = enumClassName.newEnumConstantName'");
}
var oldName = line.substring(0, equalsPos).trim();
var enumClassName = line.substring(equalsPos + 1, lastDotPos).trim();
var enumConstantName = line.substring(lastDotPos + 1).trim();
@SuppressWarnings("rawtypes")
var enumClass = Class.forName(enumClassName).asSubclass(Enum.class);
@SuppressWarnings("unchecked")
var newEnum = Enum.valueOf(enumClass, enumConstantName);
renamedEnum(oldName, newEnum);
} else {
throw new IllegalArgumentException("Renamed.ini must start with [types] or [enums]");
}
} catch (Exception ex) {
// always print message, and then continue
System.err.println("ERROR: Invalid Renamed.ini: " + url + ": " + ex.getMessage());
}
}
}
//-----------------------------------------------------------------------
@Override
public String toString() {
return "RenamedTypes" + typeRenames + ",RenamedEnumConstants" + enumRenames;
}
}