/*
* #%L
* P6Spy
* %%
* Copyright (C) 2013 P6Spy
* %%
* 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.
* #L%
*/
package com.p6spy.engine.spy.option;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import com.p6spy.engine.common.ClassHasher;
import com.p6spy.engine.common.CustomHashedHashSet;
import com.p6spy.engine.common.P6Util;
import com.p6spy.engine.logging.Category;
import com.p6spy.engine.spy.P6Factory;
public class P6OptionsRepository {
private final Map<String, Object> map = new HashMap<String, Object>();
private Set<DelayedOptionChange> delayedOptionChanges = new HashSet<DelayedOptionChange>();
private List<P6OptionChangedListener> listeners = new ArrayList<P6OptionChangedListener>();
/**
* Inidicator whether initialization has been completed. To prevent usage of not yet initialized
* properties.
*/
private boolean initCompleted = false;
public void initCompleted() {
this.initCompleted = true;
fireDelayedOptionChanges();
}
public <T> boolean set(Class<T> type, String key, Object value) {
if (value == null) {
return false;
}
return setOrUnSet(type, key, value, null);
}
public <T> boolean setOrUnSet(Class<T> type, String key, Object value, Object defaultValue) {
if (key == null || key.isEmpty()) {
throw new IllegalArgumentException("key can be neither null nor empty!");
}
if (value == null) {
value = defaultValue;
}
if (value == null) {
setInternal(key, value);
} else {
setInternal(key, parse(type, value));
}
return true;
}
<T> Object parse(Class<T> type, Object value) {
if (type.isAssignableFrom(Boolean.class)) {
return P6Util.isTrue(value.toString(), true);
} else if (type.isAssignableFrom(String.class)) {
return value.toString();
} else if (type.isAssignableFrom(Long.class)) {
return Long.parseLong(value.toString());
} else if (type.isAssignableFrom(Integer.class)) {
return Integer.parseInt(value.toString());
} else if (type.isAssignableFrom(Set.class)) {
throw new IllegalArgumentException("please call the setSet() method instead!");
} else if (type.isAssignableFrom(Collection.class) || type.isAssignableFrom(List.class)) {
throw new IllegalArgumentException("type not supported:" + type.getName());
} else if (type.isAssignableFrom(Pattern.class)) {
return Pattern.compile(value.toString());
} else if (type.isAssignableFrom(Category.class)) {
return new Category(value.toString());
} else {
// if (type.isEnum()) {
// // is sufficient for our use case where toString returns enum name
// for (T enumConstant : type.getEnumConstants()) {
// if (enumConstant.toString().equalsIgnoreCase(value.toString())) {
// return enumConstant;
// }
// }
// }
Object instance;
try {
instance = P6Util.forName(value.toString()).newInstance();
} catch (Exception ex) {
// try one more hack to load the thing
try {
ClassLoader loader = ClassLoader.getSystemClassLoader();
instance = loader.loadClass(value.toString()).newInstance();
} catch (Exception e) {
System.err.println("Cannot instantiate " + value + ", even on second attempt. ");
e.printStackTrace(System.err);
return null;
}
}
try {
T typedInstance = (T) instance;
return typedInstance;
} catch (ClassCastException e) {
System.err.println("Value " + value + ", is not of expected type. Error: " + e);
return null;
}
}
}
void setInternal(String key, Object value) {
final Object oldValue = map.put(key, value);
// propagate the changes
fireOptionChanged(key, oldValue, value);
}
@SuppressWarnings("unchecked")
public <T> boolean setSet(Class<T> type, String key, String csv) {
if (csv == null) {
return false;
}
final List<String> collection = P6Util.parseCSVList(csv);
if (collection == null) {
return false;
}
final Set<T> oldValue = getSet(type, key);
Set<T> newValue = null;
if (collection.isEmpty()) {
map.remove(key);
} else {
if (type.equals(P6Factory.class)) {
// for P6Factories the hashcode is computed based on class
newValue = new CustomHashedHashSet<T>(new ClassHasher());
} else {
newValue = new HashSet<T>();
}
for (String item : collection) {
if (item.toString().startsWith("-")) {
throw new IllegalArgumentException("- prefix has been deprecated for list-like properties! Full overriding happens (see: http://p6spy.github.io/p6spy/2.0/configandusage.html)");
}
newValue.add((T) parse(type, item));
}
map.put(key, newValue);
}
// propagate the changes
fireOptionChanged(key, oldValue, newValue);
return true;
}
void fireOptionChanged(final String key, final Object oldValue, final Object newValue) {
if (initCompleted) {
fireDelayedOptionChanges();
for (P6OptionChangedListener listener : listeners) {
listener.optionChanged(key, oldValue, newValue);
}
} else {
delayedOptionChanges.add(new DelayedOptionChange(key, oldValue, newValue));
}
}
private synchronized void fireDelayedOptionChanges() {
if (null == delayedOptionChanges) {
return;
}
for (DelayedOptionChange delayedOption : delayedOptionChanges) {
for (P6OptionChangedListener listener : listeners) {
listener.optionChanged(delayedOption.getKey(), delayedOption.getOldValue(),
delayedOption.getNewValue());
}
}
// make sure the delayed options get cleared to continue normal processing
delayedOptionChanges = null;
}
@SuppressWarnings("unchecked")
public <T> T get(Class<T> type, String key) {
if (!initCompleted) {
throw new IllegalStateException("Options didn't load completely, yet!");
}
return (T) map.get(key);
}
@SuppressWarnings("unchecked")
public <T> Set<T> getSet(Class<T> type, String key) {
return (Set<T>) map.get(key);
}
public void registerOptionChangedListener(P6OptionChangedListener listener) {
if (listener == null) {
throw new IllegalArgumentException("P6OptionChangedListener can't be null!");
}
this.listeners.add(listener);
}
public void unregisterOptionChangedListener(P6OptionChangedListener listener) {
if (listener == null) {
throw new IllegalArgumentException("P6OptionChangedListener can't be null!");
}
this.listeners.remove(listener);
}
class DelayedOptionChange {
private final String key;
private final Object oldValue;
private final Object newValue;
public DelayedOptionChange(String key, Object oldValue, Object newValue) {
super();
if (null == key || key.isEmpty()) {
throw new IllegalArgumentException("key can be neither null nor empty!");
}
this.key = key;
this.oldValue = oldValue;
this.newValue = newValue;
}
@Override
public int hashCode() {
// this is important part!
return key.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
DelayedOptionChange other = (DelayedOptionChange) obj;
if (!getOuterType().equals(other.getOuterType()))
return false;
if (key == null) {
if (other.key != null)
return false;
} else if (!key.equals(other.key))
return false;
return true;
}
public String getKey() {
return key;
}
public Object getOldValue() {
return oldValue;
}
public Object getNewValue() {
return newValue;
}
private P6OptionsRepository getOuterType() {
return P6OptionsRepository.this;
}
}
}