/* * 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; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import ca.gedge.opgraph.dag.Vertex; import ca.gedge.opgraph.exceptions.ProcessingException; import ca.gedge.opgraph.extensions.Extendable; import ca.gedge.opgraph.extensions.ExtendableSupport; /** * A node in an {@link OpGraph}. */ public abstract class OpNode implements Extendable, Vertex { /** The key for the id property */ public static final String ID_PROPERTY = "id"; /** The key for the name property */ public static final String NAME_PROPERTY = "name"; /** The key for the description property */ public static final String DESCRIPTION_PROPERTY = "description"; /** The key for the category property */ public static final String CATEGORY_PROPERTY = "category"; /** Default enabled field */ public final static InputField ENABLED_FIELD = new InputField( "enabled", "if true, disables processing of this node", true, true, Boolean.class); /** A unique id for this node */ private String id; /** The name of this node */ private String name; /** The category of this node */ private String category; /** A short description of this node and what it does */ private String description; /** The list of input fields this node has */ private List<InputField> inputFields; /** The list of output fields this node has */ private List<OutputField> outputFields; /** * Constructs a node with a generated id, this class' name as the node name * and an empty description. Also adds the enabled field. */ protected OpNode() { this(null, null, null); } /** * Constructs a node with a specified id, this class' name as the node name * and an empty description. Also adds the enabled field. * * @param id a unique id */ protected OpNode(String id) { this(id, null, null); } /** * Constructs a node with a generated id, specified name, and specified * description. Also adds the enabled field. * * @param name the name of the node * @param description the description for the node */ protected OpNode(String name, String description) { this(null, name, description); } /** * Constructs a node with a given id, name, and description. Also adds a * default "enabled" field. * * @param id a unique identifier for the node * @param name the name of the node * @param description the description for the node */ protected OpNode(String id, String name, String description) { setId(id); setName(name); setDescription(description); setCategory(null); this.outputFields = new ArrayList<OutputField>(); this.inputFields = new ArrayList<InputField>(); this.inputFields.add(ENABLED_FIELD); } /** * Gets the id for this node. * * @return the id */ public final String getId() { return id; } /** * Sets the id for this node. * * @param id the id to set */ public final void setId(String id) { if(id == null || (id = id.trim()).length() == 0) id = Integer.toHexString(System.identityHashCode(this)); if(!id.equals(this.id)) { final String oldId = this.id; this.id = id; firePropertyChange(ID_PROPERTY, oldId, this.id); } } /** * Gets a descriptive name for this node. * * @return the name */ public final String getName() { return name; } /** * Gets a default name for this node. First priority is given to the * {@link OpNodeInfo} annotation, if it is present on this node class. If not, * the node class name is used (i.e., {@link Class#getCanonicalName()}). * * @return the name */ public final String getDefaultName() { final OpNodeInfo info = getClass().getAnnotation(OpNodeInfo.class); return (info == null ? getClass().getCanonicalName() : info.name()); } /** * Sets a descriptive name for this node. * * @param name the name, or {@link #getDefaultName()} if <code>null</code> */ public final void setName(String name) { name = (name == null ? "" : name.trim()); if(name.length() == 0) name = getDefaultName(); if(!name.equals(this.name)) { final String oldName = this.name; this.name = name; firePropertyChange(NAME_PROPERTY, oldName, this.name); } } /** * Gets a short description of this node and what it does. * * @return the description */ public final String getDescription() { return description; } /** * Gets a default description for this node. First priority is given to * the {@link OpNodeInfo} annotation, if it is present on this * node's class. If not, an empty string is used. * * @return the description */ public final String getDefaultDescription() { final OpNodeInfo info = getClass().getAnnotation(OpNodeInfo.class); return (info == null ? "" : info.description()); } /** * Sets this node's category. * * @param description the description, or {@link #getDefaultDescription()} * if <code>null</code> */ public final void setDescription(String description) { if(description == null || (description = description.trim()).length() == 0) description = getDefaultDescription(); if(!description.equals(this.description)) { final String oldDescription = this.description; this.description = description; firePropertyChange(DESCRIPTION_PROPERTY, oldDescription, this.description); } } /** * Gets this node's category. * * @return the category */ public final String getCategory() { return category; } /** * Gets a default category for this node. First priority is given to the * {@link OpNodeInfo} annotation, if it is present on this node's class. * If not, an empty string is used. * * @return the description */ public final String getDefaultCategory() { final OpNodeInfo info = getClass().getAnnotation(OpNodeInfo.class); return (info == null ? "" : info.category()); } /** * Sets this node's category. * * @param category the category, or {@link #getDefaultCategory()} if * <code>null</code> */ public final void setCategory(String category) { if(category == null || (category = category.trim()).length() == 0) category = getDefaultCategory(); if(!category.equals(this.category)) { final String oldCategory = this.category; this.category = category; firePropertyChange(CATEGORY_PROPERTY, oldCategory, this.category); } } /** * Adds an input field to this node. * * @param field the input field * * @throws IllegalArgumentException if the given field will overwrite a fixed field */ public final void putField(InputField field) { if(field != null) { int index = 0; InputField foundField = null; for(; index < inputFields.size(); ++index) { if(inputFields.get(index).getKey().equals(field.getKey())) { if(inputFields.get(index).isFixed()) throw new IllegalArgumentException("Cannot overwrite fixed input field '" + field.getKey() + "' in node '" + getName() + "'"); foundField = inputFields.get(index); break; } } if(foundField == null) { inputFields.add(field); fireFieldAdded(field); } else { foundField.setDescription(field.getDescription()); foundField.setOptional(field.isOptional()); foundField.setValidator(field.getValidator()); } } } /** * Adds an output field to this node. * * @param field the input field * * @throws IllegalArgumentException if the given field will overwrite a fixed field */ public final void putField(OutputField field) { if(field != null) { int index = 0; OutputField foundField = null; for(; index < outputFields.size(); ++index) { if(outputFields.get(index).getKey().equals(field.getKey())) { if(outputFields.get(index).isFixed()) throw new IllegalArgumentException("Cannot overwrite fixed output field '" + field.getKey() + "' in node '" + getName() + "'"); foundField = outputFields.get(index); break; } } if(foundField == null) { outputFields.add(field); fireFieldAdded(field); } else { foundField.setDescription(field.getDescription()); foundField.setOutputType(field.getOutputType()); } } } /** * Removes an input field from this node. Note that {@link #ENABLED_FIELD} * cannot be removed. * * @param field the field */ public final void removeField(InputField field) { if(field != ENABLED_FIELD) { if(inputFields.remove(field)) fireFieldRemoved(field); } } /** * Removes all input fields from this node, except for {@link #ENABLED_FIELD}. */ public final void removeAllInputFields() { final ArrayList<InputField> fieldsCopy = new ArrayList<InputField>(inputFields); for(InputField field : fieldsCopy) removeField(field); } /** * Removes an output field from this node. * * @param field the field */ public final void removeField(OutputField field) { if(outputFields.remove(field)) fireFieldRemoved(field); } /** * Removes all output fields from this node. */ public final void removeAllOutputFields() { final ArrayList<OutputField> fieldsCopy = new ArrayList<OutputField>(outputFields); for(OutputField field : fieldsCopy) removeField(field); } /** * Gets the input field with a specified key * * @param key the key * * @return the input field that has the specified key, or <code>null</code> * if no input field exists with this key */ public final InputField getInputFieldWithKey(String key) { for(InputField field : inputFields) { if(field.getKey().equals(key)) return field; } return null; } /** * Gets the output field with a specified key * * @param key the key * * @return the output field that has the specified key, or <code>null</code> * if no output field exists with this key */ public final OutputField getOutputFieldWithKey(String key) { for(OutputField field : outputFields) { if(field.getKey().equals(key)) return field; } return null; } /** * Gets the list of input fields. * * @return the {@link List} of input fields (immutable) */ public final List<InputField> getInputFields() { return Collections.unmodifiableList(inputFields); } /** * Gets the list of output fields. * * @return the {@link List} of output fields (immutable) */ public final List<OutputField> getOutputFields() { return Collections.unmodifiableList(outputFields); } /** * Have this node perform its operation. * * The given {@link OpContext} contains all inputs supplied by other nodes * (if any) and can be acquired by {@link OpContext#get(Object)}. If an input * is optional, it is up to the implementing class to call {@link OpContext#containsKey(Object)} * to see if the input was supplied. Finally, computed outputs should be placed * into the given context under the appropriate {@link OutputField}s. * * @param context the working context * * @throws ProcessingException if any errors occurred during the operation */ public abstract void operate(OpContext context) throws ProcessingException; // // Extendable // private ExtendableSupport extendableSupport = new ExtendableSupport(OpNode.class); @Override public <T> T getExtension(Class<T> type) { return extendableSupport.getExtension(type); } @Override public Collection<Class<?>> getExtensionClasses() { return extendableSupport.getExtensionClasses(); } @Override public <T> T putExtension(Class<T> type, T extension) { return extendableSupport.putExtension(type, extension); } // // Listeners // private final ArrayList<OpNodeListener> listeners = new ArrayList<OpNodeListener>(); /** * Adds a listener to this node. * * @param listener the listener to add */ public void addNodeListener(OpNodeListener listener) { synchronized(listeners) { listeners.add(listener); } } /** * Removes a listener from this node. * * @param listener the listener to remove */ public void removeNodeListener(OpNodeListener listener) { synchronized(listeners) { listeners.remove(listener); } } private void firePropertyChange(String propertyName, Object oldValue, Object newValue) { synchronized(listeners) { for(OpNodeListener listener : listeners) listener.nodePropertyChanged(propertyName, oldValue, newValue); } } private void fireFieldAdded(InputField field) { synchronized(listeners) { for(OpNodeListener listener : listeners) listener.fieldAdded(this, field); } } private void fireFieldRemoved(InputField field) { synchronized(listeners) { for(OpNodeListener listener : listeners) listener.fieldRemoved(this, field); } } private void fireFieldAdded(OutputField field) { synchronized(listeners) { for(OpNodeListener listener : listeners) listener.fieldAdded(this, field); } } private void fireFieldRemoved(OutputField field) { synchronized(listeners) { for(OpNodeListener listener : listeners) listener.fieldRemoved(this, field); } } }