package org.jtheque.states.impl;
/*
* Copyright JTheque (Baptiste Wicht)
*
* 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.
*/
import org.jtheque.states.State;
import org.jtheque.states.Load;
import org.jtheque.states.Save;
import org.jtheque.states.StateService;
import org.jtheque.utils.SystemProperty;
import org.jtheque.utils.annotations.GuardedInternally;
import org.jtheque.utils.annotations.ThreadSafe;
import org.jtheque.utils.bean.ReflectionUtils;
import org.jtheque.utils.collections.CollectionUtils;
import org.jtheque.utils.io.FileUtils;
import org.jtheque.xml.utils.XMLReader;
import org.jtheque.xml.utils.XMLWriter;
import org.jtheque.xml.utils.Node;
import org.jtheque.xml.utils.XML;
import org.jtheque.xml.utils.XMLException;
import org.slf4j.LoggerFactory;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Map;
/**
* A state manager implementation.
*
* @author Baptiste Wicht
*/
@ThreadSafe
public final class StateServiceImpl implements StateService {
@GuardedInternally
private final Map<String, Object> states = CollectionUtils.newConcurrentMap(10);
@GuardedInternally
private final Map<String, Map<String, String>> properties = CollectionUtils.newConcurrentMap(5);
@GuardedInternally
private final Map<String, Collection<Node>> nodes = CollectionUtils.newConcurrentMap(5);
@Override
public <T> T getState(T state) {
Method loadMethod = checkState(state);
State stateAnnotation = state.getClass().getAnnotation(State.class);
if (stateAnnotation.delegated()) {
loadDelegatedState(state, stateAnnotation, loadMethod);
} else {
loadSimpleState(state, stateAnnotation, loadMethod);
}
states.put(stateAnnotation.id(), state);
return state;
}
/**
* Check the state for validity.
*
* @param state The state to check.
*
* @return The load method of the state.
*/
private static Method checkState(Object state) {
if (state == null) {
throw new NullPointerException("The state cannot be null");
}
if (!state.getClass().isAnnotationPresent(State.class)) {
throw new IllegalArgumentException("The state must be annotated with @State");
}
Method loadMethod = ReflectionUtils.getMethod(Load.class, state.getClass());
if (loadMethod == null) {
throw new IllegalArgumentException("The state must have a method with @Load annotation");
}
Method saveMethod = ReflectionUtils.getMethod(Save.class, state.getClass());
if (saveMethod == null) {
throw new IllegalArgumentException("The state must have a method with @Save annotation");
}
return loadMethod;
}
/**
* Return the delegated state.
*
* @param state The state to load.
* @param stateAnnotation The state annotation of the state.
* @param loadMethod The load method.
* @param <T> The type of state.
*/
private <T> void loadDelegatedState(T state, State stateAnnotation, Method loadMethod) {
Collection<Node> stateNodes = nodes.remove(stateAnnotation.id());
invokeLoadMethod(state, stateNodes, loadMethod);
}
/**
* Return the simple state.
*
* @param state The state to load.
* @param stateAnnotation The state annotation of the state.
* @param loadMethod The load method.
* @param <T> The type of state.
*/
private <T> void loadSimpleState(T state, State stateAnnotation, Method loadMethod) {
Map<String, String> stateProperties = properties.remove(stateAnnotation.id());
invokeLoadMethod(state, stateProperties, loadMethod);
}
/**
* Invoke the load method of the state.
*
* @param state The state.
* @param parameter The parameter to pass to the load method.
* @param loadMethod The load method.
*/
private static void invokeLoadMethod(Object state, Object parameter, Method loadMethod) {
try {
if (parameter != null) {
loadMethod.invoke(state, parameter);
}
} catch (IllegalAccessException e) {
throw new RuntimeException("Unable to access the @Load method of " + state, e);
} catch (InvocationTargetException e) {
throw new RuntimeException("Unable to access the @Load method of " + state, e);
}
}
/**
* Load the states. Must only be called from Spring Framework.
*/
@PostConstruct
public void loadStates() {
XMLReader<org.w3c.dom.Node> reader = XML.newJavaFactory().newReader();
if(new File(getConfigFilePath()).exists()){
try {
reader.openFile(getConfigFilePath());
for (Object stateNode : reader.getNodes("state", reader.getRootElement())) {
String id = reader.readString("@id", stateNode);
boolean delegated = reader.readBoolean("@delegated", stateNode);
if (delegated) {
Collection<org.w3c.dom.Node> nodeElements = reader.getNodes("*", stateNode);
Collection<Node> stateNodes = XML.newJavaFactory().newNodeLoader().resolveNodeStates(nodeElements);
nodes.put(id, stateNodes);
} else {
Collection<org.w3c.dom.Node> propertyElements = reader.getNodes("properties/property", stateNode);
Map<String, String> stateProperties = CollectionUtils.newHashMap(propertyElements.size());
for (Object propertyNode : propertyElements) {
stateProperties.put(reader.readString("@key", propertyNode), reader.readString("@value", propertyNode));
}
properties.put(id, stateProperties);
}
}
} catch (XMLException e) {
LoggerFactory.getLogger(getClass()).error(e.getMessage(), e);
} finally {
FileUtils.close(reader);
}
}
}
/**
* Save the states. Must only be called from Spring Framework.
*/
@PreDestroy
public void saveStates() {
XMLWriter<org.w3c.dom.Node> writer = XML.newJavaFactory().newWriter("states");
writeStates(writer);
writeProperties(writer);
writeNodes(writer);
writer.write(getConfigFilePath());
}
/**
* Write the states using the given writer.
*
* @param writer The write to use to write the states.
*/
private void writeStates(XMLWriter<org.w3c.dom.Node> writer) {
for (Map.Entry<String, Object> state : states.entrySet()) {
writer.add("state");
writer.addAttribute("id", state.getKey());
boolean delegated = state.getValue().getClass().getAnnotation(State.class).delegated();
writer.addAttribute("delegated", Boolean.toString(delegated));
Method saveMethod = ReflectionUtils.getMethod(Save.class, state.getValue().getClass());
if (delegated) {
delegatedWrite(writer, state, saveMethod);
} else {
simpleWrite(writer, state, saveMethod);
}
writer.switchToParent();
}
}
/**
* Simply writer the state with the writer.
*
* @param writer The writer to use.
* @param state The state to write.
* @param saveMethod The save method.
*/
private static void simpleWrite(XMLWriter<org.w3c.dom.Node> writer, Map.Entry<String, Object> state, Method saveMethod) {
writer.add("properties");
Map<String, String> saveProperties = getObjectsFromSaveMethod(state.getValue(), saveMethod);
for (Map.Entry<String, String> property : saveProperties.entrySet()) {
writer.add("property");
writer.addAttribute("key", property.getKey());
writer.addAttribute("value", property.getValue());
writer.switchToParent();
}
writer.switchToParent();
}
/**
* Write all the nodes of the given state.
*
* @param writer The writer to use.
* @param state The state to write.
* @param saveMethod The save method.
*/
private static void delegatedWrite(XMLWriter<org.w3c.dom.Node> writer, Map.Entry<String, Object> state, Method saveMethod) {
Iterable<Node> savedNodes = getObjectsFromSaveMethod(state.getValue(), saveMethod);
XML.newJavaFactory().newNodeSaver().writeNodes(writer, savedNodes);
}
/**
* Return the objects from the save method.
*
* @param state The state to invoke the method on.
* @param saveMethod The save method.
* @param <T> The type of objects to get.
*
* @return The objects from the save method.
*/
private static <T> T getObjectsFromSaveMethod(Object state, Method saveMethod) {
try {
return (T) saveMethod.invoke(state);
} catch (IllegalAccessException e) {
LoggerFactory.getLogger(StateServiceImpl.class).error("Unable to access the @Save method of " + state, e);
} catch (InvocationTargetException e) {
LoggerFactory.getLogger(StateServiceImpl.class).error("Unable to invoke the @Save method of " + state, e);
}
return null;
}
/**
* Write the properties to the writer.
*
* @param writer The XML writer.
*/
private void writeProperties(XMLWriter<org.w3c.dom.Node> writer) {
for (Map.Entry<String, Map<String, String>> state : properties.entrySet()) {
writer.add("state");
writer.addAttribute("id", state.getKey());
writer.addAttribute("delegated", "false");
writer.add("properties");
for (Map.Entry<String, String> property : state.getValue().entrySet()) {
writer.add("property");
writer.addAttribute("key", property.getKey());
writer.addAttribute("value", property.getValue());
writer.switchToParent();
}
writer.switchToParent();
writer.switchToParent();
}
}
/**
* Write the nodes to the writer.
*
* @param writer The writer.
*/
private void writeNodes(XMLWriter<org.w3c.dom.Node> writer) {
for (Map.Entry<String, Collection<Node>> state : nodes.entrySet()) {
writer.add("state");
writer.addAttribute("id", state.getKey());
writer.addAttribute("delegated", "true");
XML.newJavaFactory().newNodeSaver().writeNodes(writer, state.getValue());
writer.switchToParent();
}
}
/**
* Return the path to the config file.
*
* @return The path to the config file.
*/
private static String getConfigFilePath() {
return new File(SystemProperty.USER_DIR.get(), "core/config.xml").getAbsolutePath();
}
}