/* * Copyright (C) 2012 Jason Gedge <http://www.gedge.ca> * * This file is part of the OpGraph project. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package ca.gedge.opgraph.library; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; import java.util.logging.Logger; import ca.gedge.opgraph.OpNode; import ca.gedge.opgraph.OpNodeInfo; import ca.gedge.opgraph.library.handlers.URIHandler; import ca.gedge.opgraph.library.instantiators.ClassInstantiator; /** * A class for managing available node classes. * * Nodes have a corresponding {@link URI}, defined as per the following specification: * <ul> * <li> * If a node is a {@link Class} then its URI will be <code>class://classname</code>, * where <code>classname</code> is equal to {@link Class#getName()}. * </li> * <li> * If a node is a macro, then its URI will be <code>file://parentFile#macroName</code>, * where <code>parentFile</code> is equal to {@link File#getName()} and * <code>macroName</code> is the id of any macro contained in that file. * </li> * </ul> * This type can be used in the {@link #register(URI)} and {@link #get(URI)} * functions of this class. */ public class NodeLibrary implements Iterable<NodeData> { /** Logger **/ private static final Logger LOGGER = Logger.getLogger(NodeLibrary.class.getName()); /** The registered map of nodes */ private Map<URI, NodeData> nodeMap; /** The list of uri handlers this library uses */ private List< URIHandler<List<NodeData>> > uriHandlers; /** * Default constructor. */ public NodeLibrary() { this.nodeMap = new TreeMap<URI, NodeData>(); this.uriHandlers = new ArrayList<URIHandler<List<NodeData>>>(); } /** * Attempts to load the node specified by the given type. Type is parsed * as per the class' specification. If the type is a macro, and its source * file contains multiple macros, all macros in that file will be registered. * * @param uri the {@link URI} to register * * @return the node info associated with the newly registered type * * @throws IllegalArgumentException if this library cannot handle the given URI * @throws IOException if a handler handles the specified URI, but cannot load * data from that URI */ public NodeData register(URI uri) throws IOException { if(!nodeMap.containsKey(uri)) { NodeData nodeInfo = null; for(URIHandler<List<NodeData>> handler : uriHandlers) { if(handler.handlesURI(uri)) { // We catch the IOException within the loop because maybe // another handler can deal with the URI try { // Load all the node data from the given URI for(NodeData info : handler.load(uri)) { nodeInfo = info; put(info); } } catch(IOException exc) { LOGGER.warning(handler.getClass() + " says it handles URI '" + uri + "', but threw an IOException"); } } } if(nodeInfo == null) throw new IllegalArgumentException("The URI '" + uri + "' is not handled by this library"); } return get(uri); } /** * Registers a specified {@link OpNode} class to this library. * * @param clz the class * * @throws URISyntaxException if a URI could not be created for the given class */ public void register(Class<? extends OpNode> clz) throws URISyntaxException { if(clz != null) { final OpNodeInfo nodeInfo = clz.getAnnotation(OpNodeInfo.class); if(nodeInfo != null) { final String type = clz.getName(); final String name = (nodeInfo == null ? "no name" : nodeInfo.name()); final String desc = (nodeInfo == null ? "" : nodeInfo.description()); final String cat = (nodeInfo == null ? "" : nodeInfo.category()); final URI uri = new URI("class", type, null); put(new NodeData(uri, name, desc, cat, new ClassInstantiator<OpNode>(clz))); } } } /** * Unregisters a URI from this library. * * @param uri the uri to unregister * * @return the node info associated with the given uri, or <code>null</code> * if the given uri was not registered with this library */ public NodeData unregister(URI uri) { final NodeData info = nodeMap.remove(uri); if(info != null) fireNodeUnregistered(info); return info; } /** * Adds a specific node type to this library. * * @param info the node info * * @throws NullPointerException if given info is <code>null</code>, or any * of its members are <code>null</code> * * @throws IllegalArgumentException if no validator exists to handle the * URI in the given info */ public void put(NodeData info) { if(info == null) throw new NullPointerException("Node info cannot be null"); if(info.uri == null || info.name == null || info.description == null || info.instantiator == null) throw new NullPointerException("Members of node info cannot be null"); // Check to make sure we have a handler for the given URI boolean handled = false; for(URIHandler<?> handler : uriHandlers) { if(handler.handlesURI(info.uri)) { handled = true; break; } } if(!handled) throw new IllegalArgumentException("No handler exists for the uri: " + info.uri); nodeMap.put(info.uri, info); fireNodeRegistered(info); } /** * Gets the node info for every node type registered with this library. * * @return a collection of node info */ public List<NodeData> getNodeInfo() { List<NodeData> list = new ArrayList<NodeData>(nodeMap.values()); return Collections.unmodifiableList(list); } /** * Gets node information associated with a specified type. * * @param uri the uri of the node * * @return the information associated with the given type, or <code>null</code> * if the given uri is not registered with this library. * * @see #register(URI) */ public NodeData get(URI uri) { return nodeMap.get(uri); } /** * Get's the URI associated with a node. * * @param node the node * * @return the URI for the specified node * * @throws URISyntaxException if the given node could not be encoded into a URI */ public static URI getNodeURI(OpNode node) throws URISyntaxException { // FIXME since maven // if(node instanceof MacroNode) { // final MacroNode macro = (MacroNode)node; // if(macro.getSource() == null) // return new URI(null, null, macro.getGraph().getId()); // return new URI("file", macro.getSource().getPath(), macro.getGraph().getId()); // } else { return new URI("class", node.getClass().getName(), null); // } } /** * Adds the given handler to the list of handlers this library uses. * * @param handler the handler */ public void addURIHandler(URIHandler<List<NodeData>> handler) { if(handler != null) uriHandlers.add(handler); } /** * Removes the given handler from the list of handlers this library uses. * * @param handler the handler */ public void removeURIHandler(URIHandler<List<NodeData>> handler) { uriHandlers.remove(handler); } /** * Gets a mapping from category name to all the nodes having that category. * * @return a {@link SortedMap} from category name to a {@link List} of * {@link NodeData} instances having that category. */ public SortedMap<String, List<NodeData>> getCategoryMap() { final SortedMap<String, List<NodeData>> categoryMap = new TreeMap<String, List<NodeData>>(); for(NodeData nodeInfo : nodeMap.values()) { // Grab the list of NodeData instances for this NodeData's category, // but if this is the first time hitting this category, create a new // list and put it into the map List<NodeData> nodes = categoryMap.get(nodeInfo.category); if(nodes == null) { nodes = new ArrayList<NodeData>(); categoryMap.put(nodeInfo.category, nodes); } nodes.add(nodeInfo); } return categoryMap; } // // Listeners // private ArrayList<NodeLibraryListener> listeners = new ArrayList<NodeLibraryListener>(); /** * Adds a listener to this library. * * @param listener the listener to add */ public void addNodeLibraryListener(NodeLibraryListener listener) { synchronized(listeners) { if(listener != null && !listeners.contains(listener)) listeners.add(listener); } } /** * Removes a listener from this library. * * @param listener the listener to remove */ public void removeNodeLibraryListener(NodeLibraryListener listener) { synchronized(listeners) { listeners.remove(listener); } } protected void fireNodeRegistered(NodeData info) { synchronized(listeners) { for(NodeLibraryListener listener : listeners) listener.nodeRegistered(info); } } protected void fireNodeUnregistered(NodeData info) { synchronized(listeners) { for(NodeLibraryListener listener : listeners) listener.nodeUnregistered(info); } } // // Iterable // @Override public Iterator<NodeData> iterator() { return Collections.unmodifiableCollection(nodeMap.values()).iterator(); } }