/***************************************************
*
* cismet GmbH, Saarbruecken, Germany
*
* ... and it just works.
*
****************************************************/
package Sirius.navigator.ui.tree;
import Sirius.navigator.NavigatorConcurrency;
import Sirius.navigator.types.treenode.DefaultMetaTreeNode;
import Sirius.navigator.types.treenode.RootTreeNode;
import Sirius.navigator.types.treenode.WaitTreeNode;
import Sirius.navigator.ui.ComponentRegistry;
import Sirius.server.middleware.types.Node;
import org.apache.log4j.Logger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
/**
* DOCUMENT ME!
*
* @author martin.scholl@cismet.de
* @version $Revision$, $Date$
*/
public final class MetaTreeRefreshCache implements TreeModelListener {
//~ Static fields/initializers ---------------------------------------------
private static final transient Logger LOG = Logger.getLogger(MetaTreeRefreshCache.class);
private static final transient Pattern WC_PATTERN = Pattern.compile("(?<!\\\\)\\*{1}|(?<!\\\\)\\?{1}"); // NOI18N
//~ Instance fields --------------------------------------------------------
private final transient Map<String, DefaultMetaTreeNode> nodeCache;
private final transient ExecutorService cacheUpdateDispatcher;
// we don't need sync on this var since wrong states are tolerable
private transient boolean valid;
//~ Constructors -----------------------------------------------------------
/**
* Creates a new MetaTreeRefreshCache object.
*/
public MetaTreeRefreshCache() {
valid = true;
nodeCache = new HashMap<String, DefaultMetaTreeNode>();
cacheUpdateDispatcher = Executors.newSingleThreadExecutor(NavigatorConcurrency.createThreadFactory(
"meta-tree-refresh-cache-update-dispatcher", // NOI18N
new CacheExceptionHandler()));
}
//~ Methods ----------------------------------------------------------------
/**
* The cache allows wildcards in the artificalId string. There are two wildcard characters:
*
* @param artificialId DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public Set<DefaultMetaTreeNode> get(final String artificialId) {
if (valid) {
final Set<DefaultMetaTreeNode> nodes = new HashSet<DefaultMetaTreeNode>();
final Matcher m = WC_PATTERN.matcher(artificialId);
if (m.find()) {
m.reset();
final StringBuilder regexbuilder = new StringBuilder();
int beginIndex = 0;
while (m.find()) {
regexbuilder.append(Pattern.quote(artificialId.substring(beginIndex, m.start())));
regexbuilder.append((artificialId.charAt(m.start()) == '*') ? ".*" : ".?"); // NOI18N
beginIndex = m.end();
}
if (beginIndex < artificialId.length()) {
regexbuilder.append(artificialId.substring(beginIndex));
}
final Pattern regex = Pattern.compile(regexbuilder.toString());
for (final String key : nodeCache.keySet()) {
if (regex.matcher(key).matches()) {
nodes.add(nodeCache.get(key));
}
}
} else {
final DefaultMetaTreeNode node = nodeCache.get(artificialId);
if (node != null) {
nodes.add(node);
}
}
return nodes;
} else {
LOG.warn("cache is invalid, tree ui probably not accurate anymore, perform manual tree refresh"); // NOI18N
}
return null;
}
/**
* The cache will be cleared and initialised with the currently expanded paths.
*/
private void init() {
if (LOG.isDebugEnabled()) {
LOG.debug("init refresh cache"); // NOI18N
}
valid = true;
nodeCache.clear();
// NOTE: Maybe we want to use a reference initialised by the constructor to not bind this implementation to the
// current registry implementation and/or the navigator at all. If so consider to change the DefaultMetaTreeNode
// (or at least extract an interface) to minimise/loosen dependencies.
final MetaCatalogueTree tree = ComponentRegistry.getRegistry().getCatalogueTree();
final Object root = tree.getModel().getRoot();
if (root == null) {
LOG.warn("cannot initialise tree refresh cache, empty root"); // NOI18N
return;
}
final TreePath path = new TreePath(root);
final Enumeration<TreePath> paths = tree.getExpandedDescendants(path);
if (paths == null) {
if (LOG.isDebugEnabled()) {
LOG.debug("nothing to initialise, root not expanded"); // NOI18N
}
return;
}
final List nodes = new ArrayList();
while (paths.hasMoreElements()) {
nodes.add(paths.nextElement().getLastPathComponent());
}
cacheUpdateDispatcher.submit(new CacheUpdater(nodes.toArray(), true));
}
@Override
public void treeNodesChanged(final TreeModelEvent e) {
// not needed currently, probably interface an advanced state of refresh mechanism this will be handled
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public boolean isValid() {
return valid;
}
@Override
public void treeNodesInserted(final TreeModelEvent e) {
if (!valid) {
LOG.warn("cache is invalid, won't update cache, need reinit"); // NOI18N
return;
}
final Object[] inserted = e.getChildren();
if (inserted == null) {
// nothing to do
return;
}
cacheUpdateDispatcher.submit(new CacheUpdater(inserted, true));
}
@Override
public void treeNodesRemoved(final TreeModelEvent e) {
if (!valid) {
LOG.warn("cache is invalid, won't update cache, need reinit"); // NOI18N
return;
}
final Object[] removed = e.getChildren();
if (removed == null) {
// nothing to do
return;
}
cacheUpdateDispatcher.submit(new CacheUpdater(removed, false));
}
@Override
public void treeStructureChanged(final TreeModelEvent e) {
if (e.getPath().length == 1) {
// root was changed, first clear the current cache,
// then scan all open paths for artificial ids since the current hard refresh operations use "explore" to
// expand the tree path to the previously selected node and afterwards fire a structure changed with the
// root node as origin
init();
} else if (!valid) {
LOG.warn("cache is invalid, won't update cache, need reinit"); // NOI18N
} else {
// unfortunately Navigator fires tree structure changed and not any inserted/removed events
final Object o = e.getTreePath().getLastPathComponent();
if (o instanceof TreeNode) {
if (o instanceof WaitTreeNode) {
// ignore
return;
}
final TreeNode dmtn = (TreeNode)o;
final List children = Collections.list(dmtn.children());
if (children.isEmpty()) {
return;
}
cacheUpdateDispatcher.submit(new CacheUpdater(children.toArray(), true));
} else {
LOG.warn("illegal node in tree: " + o); // NOI18N
}
}
}
//~ Inner Classes ----------------------------------------------------------
/**
* DOCUMENT ME!
*
* @version $Revision$, $Date$
*/
private final class CacheUpdater implements Runnable {
//~ Instance fields ----------------------------------------------------
private final transient Object[] nodes;
private final transient boolean insert;
//~ Constructors -------------------------------------------------------
/**
* Creates a new CacheUpdater object.
*
* @param nodes DOCUMENT ME!
* @param insert DOCUMENT ME!
*/
public CacheUpdater(final Object[] nodes, final boolean insert) {
this.nodes = nodes;
this.insert = insert;
}
//~ Methods ------------------------------------------------------------
@Override
public void run() {
for (final Object o : nodes) {
if (o instanceof DefaultMetaTreeNode) {
if (o instanceof WaitTreeNode) {
// ignore WaitTreeNodes, they're only temporary
continue;
} else if (o instanceof RootTreeNode) {
// ignore RootTreeNode, it's not of interest to refresh requests
continue;
}
final DefaultMetaTreeNode dmtn = (DefaultMetaTreeNode)o;
if (dmtn.getNode() == null) {
LOG.warn("DefaultMetaTreeNode without backing Node: " + o); // NOI18N
} else {
final Node node = dmtn.getNode();
final String artificialId = node.getArtificialId();
if (artificialId != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("found artificial id for node, updating cache [node=" + node // NOI18N
+ "|artificalId=" // NOI18N
+ artificialId + "]"); // NOI18N
}
// as tree structure changed is also an insert, we should additionally inspect, if the node
// to insert is deep equal to the already present node
final DefaultMetaTreeNode cacheNode = nodeCache.get(artificialId);
if ((cacheNode != null) == insert) {
if ((cacheNode != null) && cacheNode.deepEquals(dmtn)) {
// there was a structural change, but the node itself was not changed at all, we can
// ignore the change and go on
if (LOG.isDebugEnabled()) {
LOG.debug("ignoring already present node: " + dmtn); // NOI18N
}
continue;
} else {
valid = false;
nodeCache.clear();
final String keyword = insert ? "already" : "not"; // NOI18N
final String message = "the artificial id is " + keyword + " in cache, " // NOI18N
+ "cache corrupt or illegal tree, invalidating cache: " // NOI18N
+ artificialId;
LOG.error(message);
throw new IllegalStateException(message);
}
}
if (insert) {
nodeCache.put(artificialId, dmtn);
} else {
nodeCache.remove(artificialId);
}
}
}
} else {
LOG.warn("received unknown node type: " + o); // NOI18N
}
}
}
}
/**
* DOCUMENT ME!
*
* @version $Revision$, $Date$
*/
private static final class CacheExceptionHandler implements Thread.UncaughtExceptionHandler {
//~ Static fields/initializers -----------------------------------------
private static final transient Logger LOG = Logger.getLogger(CacheExceptionHandler.class);
//~ Methods ------------------------------------------------------------
@Override
public void uncaughtException(final Thread t, final Throwable e) {
if (e instanceof Error) {
LOG.fatal("encountered error in thread: " + t, e);
throw (Error)e;
} else {
LOG.error("encountered exception in refresh cache, " // NOI18N
+ "tree ui will probably be not accurate anymore, thread: " + t, // NOI18N
e);
}
}
}
}