/*******************************************************************************
* Copyright 2014 Analog Devices, 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.analog.lyric.options;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;
import org.eclipse.jdt.annotation.Nullable;
import com.google.common.collect.AbstractIterator;
import com.google.common.collect.ObjectArrays;
/**
* A registry of known option keys indexed by qualified name.
* <p>
* For use in looking up option keys by string either for use in configuration files or in
* implementing external APIs (e.g. MATLAB).
* <p>
* @since 0.07
*/
@ThreadSafe
public class OptionRegistry implements Iterable<IOptionKey<?>>
{
/*-------
* State
*/
/**
* Maps canonical class names to {@link OptionKeys}.
* <p>
* Writes are protected by lock. Reads are not.
*/
@GuardedBy("this")
private final ConcurrentNavigableMap<String, OptionKeys> _canonicalMap;
/**
* Maps simple class names to {@link OptionKeys}
* <p>
* Unlike {@link #_canonicalMap}, this may map name to keys for multiple
* classes with the same simple name.
* <p>
* Writes are protected by lock. Reads are not.
*/
@GuardedBy("this")
private final ConcurrentNavigableMap<String, OptionKeys[]> _simpleMap;
private final boolean _autoLoad;
@GuardedBy("this")
private volatile int _size;
private static final int publicStatic = Modifier.PUBLIC | Modifier.STATIC;
/*--------------
* Construction
*/
/**
* Create a new empty registry.
* <p>
* @param autoLoad determines setting of {@link #autoLoadKeys()}.
* @since 0.07
*/
public OptionRegistry(boolean autoLoad)
{
_autoLoad = autoLoad;
_canonicalMap = new ConcurrentSkipListMap<String, OptionKeys>();
_simpleMap = new ConcurrentSkipListMap<String,OptionKeys[]>();
}
/**
* Create a new empty registry with autoloading enabled.
* @see #OptionRegistry(boolean)
* @since 0.07
*/
public OptionRegistry()
{
this(true);
}
/*------------------
* Iterable methods
*/
/**
* Iterates over option keys that are currently registered with this object in
* order of {@linkplain OptionKey#canonicalName(IOptionKey) canonical name}.
*/
@Override
public Iterator<IOptionKey<?>> iterator()
{
return new OptionIterator();
}
private class OptionIterator extends AbstractIterator<IOptionKey<?>>
{
private final Iterator<OptionKeys> _keysIterator;
private volatile Iterator<IOptionKey<?>> _keyIterator;
private OptionIterator()
{
_keysIterator = _canonicalMap.values().iterator();
_keyIterator = Collections.emptyIterator();
}
@Override
protected @Nullable IOptionKey<?> computeNext()
{
Iterator<IOptionKey<?>> keyIter = _keyIterator;
while (!keyIter.hasNext())
{
if (_keysIterator.hasNext())
{
_keyIterator = keyIter = _keysIterator.next().values().iterator();
}
else
{
endOfData();
return null;
}
}
return keyIter.next();
}
}
/*------------------------
* OptionRegistry methods
*/
/**
* Adds option keys to registry.
* <p>
* @return true if registry was changed (keys were not already in registry).
* @since 0.07
*/
public boolean add(OptionKeys keys)
{
Class<?> declaringClass = keys.declaringClass();
synchronized (this)
{
if (null != _canonicalMap.put(declaringClass.getCanonicalName(), keys))
{
return false;
}
OptionKeys[] array = new OptionKeys[] { keys };
final String classname = declaringClass.getSimpleName();
OptionKeys[] existingArray = _simpleMap.get(classname);
if (existingArray == null)
{
_simpleMap.put(classname, array);
}
else
{
_simpleMap.put(classname, ObjectArrays.concat(existingArray, keys));
}
_size += keys.values().size();
}
return true;
}
/**
* Registers statically declared option key instances found reflectively in specified classes.
* Adds all statically declared, final fields of type {@link IOptionKey} whose {@linkplain IOptionKey#name() name}
* attribute matches its declared name. Also will recursively add from public nested classes.
*
* @return the number of unique option keys that were added
* @since 0.07
*/
public int addFromClasses(Class<?> ... declaringClasses)
{
int nAdded = 0;
for (Class<?> declaringClass : declaringClasses)
{
OptionKeys keys = OptionKeys.declaredInClass(declaringClass);
int nAddedFromClass = keys.values().size();
if (nAddedFromClass > 0 && !add(keys))
{
nAddedFromClass = 0;
}
for (Class<?> innerClass : declaringClass.getDeclaredClasses())
{
if ((innerClass.getModifiers() & publicStatic) == publicStatic)
{
nAddedFromClass += addFromClasses(innerClass);
}
}
nAdded += nAddedFromClass;
}
return nAdded;
}
/**
* Returns key for given qualified name or throws an error.
* <p>
* @param keyOrName either a {@link IOptionKey} instance which will simply be returned or
* a {@link String} compatible with {@link #get(String)}.
* @throws NoSuchElementException if key not found or input argument is not the correct type.
* @throws IllegalArgumentException if {@code keyOrName} does not have the correct type.
* @since 0.07
*/
public IOptionKey<?> asKey(Object keyOrName)
{
IOptionKey<?> key;
if (keyOrName instanceof String)
{
String name = (String)keyOrName;
key = get(name);
if (key == null)
{
throw new NoSuchElementException(String.format("Unknown option key '%s'", name));
}
else
{
return key;
}
}
else if (keyOrName instanceof IOptionKey)
{
key = (IOptionKey<?>)keyOrName;
}
else
{
throw new IllegalArgumentException(
String.format("Expected String or IOptionKey instead of '%s'",
keyOrName.getClass().getSimpleName()));
}
return key;
}
/**
* If true, registry will attempt to automatically load fully qualified keys from classes.
* <p>
* For instance, if attempting to get key for the string:
*
* <blockquote>
* "com.mycompany.MyOptions.myOption"
* </blockquote>
*
* the registry will automatically load all option keys declared in the class {@code MyOptions}.
* This will only work for fully qualified names whose classes are on the class path.
* <p>
* This attribute is set in the constructor.
* @since 0.07
*/
public boolean autoLoadKeys()
{
return _autoLoad;
}
/**
* Returns option key with specified name or null if not found.
* <p>
* If {@link #autoLoadKeys()} is enabled and {@code name} is a full canonical name then
* the key will be loaded automatically by loading the class and adding all of its declared
* options.
* <p>
* @param name is either the {@linkplain OptionKey#canonicalName(IOptionKey) canonical name} or
* {@linkplain OptionKey#qualifiedName(IOptionKey) qualified name} of the option.
* @throws AmbiguousOptionNameException if more than one possible option key matches
* the given name. This will not happen if a canonical name is used.
* The list of possible matches can be found in the exception.
* @see OptionKey#canonicalName(IOptionKey)
* @since 0.07
*/
public @Nullable IOptionKey<?> get(String name)
{
int i = name.lastIndexOf('.');
if (i >= 0)
{
String className = name.substring(0, i);
String optionName = name.substring(i + 1);
if (className.indexOf('.') >= 0)
{
// Should be canonical class name
OptionKeys keys = _canonicalMap.get(className);
if (keys == null && _autoLoad)
{
try
{
Class<?> c = Class.forName(className, false, getClass().getClassLoader());
add(keys = OptionKeys.declaredInClass(c));
}
catch (ClassNotFoundException ex)
{
}
}
if (keys != null)
{
return keys.get(optionName);
}
}
else
{
OptionKeys[] keysArray = _simpleMap.get(className);
if (keysArray != null)
{
if (keysArray.length == 1)
{
return keysArray[0].get(optionName);
}
else
{
ArrayList<IOptionKey<?>> options = new ArrayList<IOptionKey<?>>();
for (OptionKeys keys : keysArray)
{
IOptionKey<?> option = keys.get(optionName);
if (option != null)
{
options.add(option);
}
}
switch (options.size())
{
case 0:
break;
case 1:
return options.get(0);
default:
throw new AmbiguousOptionNameException(name, options);
}
}
}
}
}
return null;
}
/**
* Returns a newly constructed list of keys matching regular expression.
* <p>
* The will contain all keys whose {@linkplain OptionKey#qualifiedName(IOptionKey) qualified name} matches the
* given regular expression pattern sorted by name.
* @since 0.07
*/
public ArrayList<IOptionKey<?>> getAllMatching(Pattern pattern)
{
Matcher matcher = pattern.matcher("");
ArrayList<IOptionKey<?>> results = new ArrayList<IOptionKey<?>>();
for (IOptionKey<?> key : this)
{
matcher.reset(OptionKey.qualifiedName(key));
if (matcher.matches())
{
results.add(key);
}
}
return results;
}
/**
* Returns a newly constructed list of keys matching regular expression.
* <p>
* The will contain all keys whose {@linkplain OptionKey#qualifiedName(IOptionKey) qualified name} matches the
* given regular expression pattern sorted by name.
* @since 0.07
*/
public ArrayList<IOptionKey<?>> getAllMatching(String regexp)
{
return getAllMatching(Pattern.compile(regexp));
}
/**
* Iterates over {@link OptionKeys} registered with this object.
* @since 0.07
*/
public Collection<OptionKeys> getOptionKeys()
{
return Collections.unmodifiableCollection(_canonicalMap.values());
}
/**
* The number of entries in the registry.
* <p>
* @since 0.07
*/
public int size()
{
return _size;
}
}