/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 gobblin.util;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.reflections.Reflections;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import gobblin.annotation.Alias;
/**
* A class that scans through all classes in the classpath annotated with {@link Alias} and is a subType of <code>subTypeOf</code> constructor argument.
* It caches the alias to class mapping. Applications can call {@link #resolve(String)} to resolve a class's alias to its
* cannonical name.
*
* <br>
* <b>Note : If same alias is used for multiple classes in the classpath, the first mapping in classpath scan holds good.
* Subsequent ones will be logged and ignored.
* </b>
*
* <br>
* <b>
* Note: For the moment this class will only look for classes with gobblin prefix, as scanning the entire classpath is
* very expensive.
* </b>
*/
@Slf4j
public class ClassAliasResolver<T> {
// Scan all packages in the classpath with prefix gobblin when class is loaded.
// Since scan is expensive we do it only once when class is loaded.
private static final Reflections REFLECTIONS = new Reflections("gobblin");
Map<String, Class<? extends T>> aliasToClassCache;
private final List<Alias> aliasObjects;
private final Class<T> subtypeOf;
public ClassAliasResolver(Class<T> subTypeOf) {
Map<String, Class<? extends T>> cache = Maps.newHashMap();
this.aliasObjects = Lists.newArrayList();
for (Class<? extends T> clazz : REFLECTIONS.getSubTypesOf(subTypeOf)) {
if (clazz.isAnnotationPresent(Alias.class)) {
Alias aliasObject = clazz.getAnnotation(Alias.class);
String alias = aliasObject.value().toUpperCase();
if (cache.containsKey(alias)) {
log.warn(String.format("Alias %s already mapped to class %s. Mapping for %s will be ignored", alias,
cache.get(alias).getCanonicalName(), clazz.getCanonicalName()));
} else {
aliasObjects.add(aliasObject);
cache.put(clazz.getAnnotation(Alias.class).value().toUpperCase(), clazz);
}
}
}
this.subtypeOf = subTypeOf;
this.aliasToClassCache = ImmutableMap.copyOf(cache);
}
/**
* Resolves the given alias to its name if a mapping exits. To create a mapping for a class,
* it has to be annotated with {@link Alias}
*
* @param possibleAlias to use for resolution.
* @return The name of the class with <code>possibleAlias</code> if mapping is available.
* Return the input <code>possibleAlias</code> if no mapping is found.
*/
public String resolve(final String possibleAlias) {
if (this.aliasToClassCache.containsKey(possibleAlias.toUpperCase())) {
return this.aliasToClassCache.get(possibleAlias.toUpperCase()).getName();
}
return possibleAlias;
}
/**
* Attempts to resolve the given alias to a class. It first tries to find a class in the classpath with this alias
* and is also a subclass of {@link #subtypeOf}, if it fails it returns a class object for name
* <code>aliasOrClassName</code>.
*/
public Class<? extends T> resolveClass(final String aliasOrClassName) throws ClassNotFoundException {
if (this.aliasToClassCache.containsKey(aliasOrClassName.toUpperCase())) {
return this.aliasToClassCache.get(aliasOrClassName.toUpperCase());
}
try {
return Class.forName(aliasOrClassName).asSubclass(this.subtypeOf);
} catch (ClassCastException cce) {
throw new ClassNotFoundException(
String.format("Found class %s but it cannot be cast to %s.", aliasOrClassName, this.subtypeOf.getName()), cce);
}
}
/**
* Get the map from found aliases to classes.
*/
public Map<String, Class<? extends T>> getAliasMap() {
return ImmutableMap.copyOf(this.aliasToClassCache);
}
public List<Alias> getAliasObjects() {
return ImmutableList.copyOf(this.aliasObjects);
}
}