001/*
002 *                    BioJava development code
003 *
004 * This code may be freely distributed and modified under the
005 * terms of the GNU Lesser General Public Licence.  This should
006 * be distributed with the code.  If you do not have a copy,
007 * see:
008 *
009 *      http://www.gnu.org/copyleft/lesser.html
010 *
011 * Copyright for this code is held jointly by the individual
012 * authors.  These should be listed in @author doc comments.
013 *
014 * For more information on the BioJava project and its aims,
015 * or to join the biojava-l mailing list, visit the home page
016 * at:
017 *
018 *      http://www.biojava.org/
019 *
020 */
021
022package org.biojava.bio.program.tagvalue;
023
024import java.util.HashMap;
025import java.util.Map;
026
027import org.biojava.utils.ParserException;
028
029/**
030 * This class implements a state machine for parsing events from
031 * the Parser class.
032 * </p>
033 * <p>
034 * Each State can be specified to deliver events
035 * to a particular TagValueListener.
036 * </p>
037 * <p>
038 * Transitions can be specified to occur between States when
039 * specific events are encountered. These events can be
040 * Tags delivered by startTag as well as the endTag/endRecord
041 * events.  Events that result in exit from the current State
042 * can be specified to be notifiable to the State listener.
043 * </p>
044 * <p>
045 * In addition, tables of transitions can be defined and specified
046 * as the fallback when the a corresponding Transition cannot be
047 * found for the tag.  This is useful for specifying the destination
048 * States that are globally applicable for particular tags.  As the
049 * fallbacks can be chained, you can end up with a hierarchy of
050 * Transition Tables, some being State-specific, others applicable to
051 * groups of States and finally one being global.
052 * </p>
053 * @author David Huen
054 */
055public class StateMachine
056        implements TagValueWrapper
057{
058        protected static final String START_RECORD_TAG = "__START_RECORD_TAG__";
059        protected static final String END_RECORD_TAG = "__END_RECORD__";
060        protected static final String END_TAG = "__END_TAG__";
061        protected static final String MAGICAL_STATE = "__MAGICAL__";
062
063        protected TagValueListener delegate = null;
064
065        /**
066         * Interface implemented by State listeners that
067         * want notification when a transition leaves the State.
068         */
069        public interface ExitNotification
070        {
071                public void notifyExit() throws ParserException;
072        }
073
074        /**
075         * Interface for a State within this StateMachine
076         */
077        public interface State
078        {
079        public String getLabel();
080            public TagValueListener getListener();
081//      public void setListener(TagValueListener listener);
082//          public void setTransition(Object tag, State destination);
083//      public void setDefaultTransition(State defaultDestination);
084            public void transit(Object tag) throws ParserException;
085        }
086
087        /**
088         * class to represent a State Transition
089         */
090        public class Transition
091        {
092                /**
093                 * the terminus of this Transition
094                 */
095                public State destination = null;
096
097                /**
098                 * should the State listener be notified
099                 * that this transition is leaving the state.
100                 */
101                public boolean notifyOnExit = false;
102
103                private Transition(State destination, boolean notifyOnExit)
104                {
105                        this.destination = destination;
106                        this.notifyOnExit = notifyOnExit;
107                }
108        }
109
110        /**
111         * Table of Transition destination States
112         * and their corresponding Tags.
113         * <p>
114         * Note that you can chain a series of these
115         * Transition tables and the lookup will
116         * proceed along the chain until it succeeds.
117         */
118        public class TransitionTable
119        {
120                private Map table = new HashMap();
121                private TransitionTable fallback = null;
122
123                protected TransitionTable() {}
124
125                /**
126                 * set a Transition within this TransitionTable (2-argument form)
127                 */
128                public void put(Object tag, Transition transition)
129                        throws ParserException
130                {
131                        if (table.containsKey(tag))
132                                throw new ParserException("duplicate tag " + tag);
133
134                        table.put(tag, transition);
135                }
136
137                /**
138                 * set a Transition within this TransitionTable (3-argument form)
139                 */
140                public void setTransition(Object tag, State destination, boolean notifyOnExit)
141                        throws ParserException
142                {
143                        put(tag, new Transition(destination, notifyOnExit));
144                }
145
146                /**
147                 * get the Transition associated with the specified tag.
148                 */
149                public Transition get(Object tag)
150                {
151                        Transition transition =  (Transition) table.get(tag);
152
153                        if ((transition == null) && (fallback != null)) {
154                                // do a fallback lookup
155                                transition = fallback.get(tag);
156                        }
157
158                        return transition;
159                }
160
161                /**
162                 * set the specified TransitionTable to be looked
163                 * looked up if the Transition cannot be found in
164                 * this one.
165                 */
166                public void setFallback(TransitionTable fallback)
167                {
168                        this.fallback = fallback;
169                }
170        }
171
172
173        /**
174         * Implementation of a State in a state machine
175         */
176        public class BasicState
177                implements State
178        {
179                private String label;
180                private TransitionTable transitions;
181                private TransitionTable defaultTransitions;
182                private TagValueListener listener = null;
183
184                /**
185                 * This is the default constructor
186                 */
187                public BasicState(String label)
188                {
189                        this.label = label;
190                        this.transitions = new TransitionTable();
191                }
192
193                /**
194                 * when this constructor is used, a fixed listener
195                 * is used with this state.  When setListener is called
196                 * on this class, it is the listener of this fixed
197                 * listener that is changed.
198                 */
199                public BasicState(String label, TagValueListener listener)
200                {
201                        this.label = label;
202                        this.listener = listener;
203                        this.transitions = new TransitionTable();
204                }
205
206                /**
207                 * return the label of this class.
208                 */
209                public String getLabel() { return label; }
210
211                /**
212                 * return the TagValueListener assigned to this State.
213                 */
214                public TagValueListener getListener() { return listener; }
215
216                /**
217                 * set a TagValueListener for this State.
218                 */
219                public void setListener(TagValueListener listener)
220                {
221                        this.listener = listener;
222                }
223
224                /**
225                 * set a Transition for this State
226                 */
227                public void setTransition(Object tag, State destination, boolean notifyOnExit)
228                        throws ParserException
229                {
230                        transitions.setTransition(tag, destination, notifyOnExit);
231                }
232
233                /**
234                 * set a Transition for this State setting notifyOnExit to false.
235                 */
236                public void setTransition(Object tag, State destination)
237                        throws ParserException
238                {
239                        setTransition(tag, destination, false);
240                }
241
242                /**
243                 * retrieve the TransitionTable for this State.
244                 */
245                public TransitionTable getTransitionTable()
246                {
247                        return transitions;
248                }
249
250                /**
251                 * specify fallback TransitionTable for this State
252                 */
253                public void setDefaultTransitions(TransitionTable defaultTransitions)
254                {
255                        this.defaultTransitions = defaultTransitions;
256                }
257
258                /**
259                 * Find the destination State when the specified tag
260                 * is encountered.  If the destination is successfully
261                 * obtained, the listener is also notified of exit if
262                 * required.
263                 */
264                public void transit(Object tag)
265                        throws ParserException
266                {
267
268                        Transition transition = transitions.get(tag);
269
270                        if (transition == null) {
271                                // explicit transition unavailable.
272                                // Use fallback default.
273                                if (defaultTransitions == null) 
274                                        throw new ParserException("no transition available from " + statePointer.getLabel() + " for tag '" + tag + "'");
275
276                                transition = defaultTransitions.get(tag);
277
278                                if (transition == null)
279                                        throw new ParserException("no transition available from " + statePointer.getLabel() + " for tag '" + tag + "'");
280                        }
281
282                        State nextState = transition.destination;
283
284                        if (transition.notifyOnExit) {
285//                              System.out.println("notifyOnExit being checked. this is " + this.getLabel() + " next is " + nextState.getLabel());
286                                if (nextState != this) {
287                                        if (listener instanceof ExitNotification) {
288                                                ((ExitNotification) listener).notifyExit();
289                                        }
290                                }
291                        }
292
293                        statePointer = nextState;
294                }
295        }
296
297        /**
298         * a basic listener for a State.  It forwards all events to the
299         * delegate for the StateMachine.  Extend to implement listeners
300         * for specific states.
301         */
302        public class SimpleStateListener
303                implements TagValueListener
304        {
305                private boolean exceptionOnNullDelegate = true;
306
307                /**
308                 * determines if an exception is thrown when an event
309                 * arrives without the delegate being set.  Default
310                 * is that a ParserException is thrown.
311                 i*/
312                public void setExceptionOnNullDelegate(boolean throwException)
313                { exceptionOnNullDelegate = throwException; }
314
315                public void startTag(Object tag)
316                        throws ParserException 
317                { 
318                        if (delegate != null)
319                                delegate.startTag(tag);
320                        else if (exceptionOnNullDelegate)
321                                throw new ParserException("event arrived without a delegate being specified");
322                }
323
324                public void endTag() 
325                        throws ParserException 
326                {
327                        if (delegate != null)
328                                delegate.endTag();
329                        else if (exceptionOnNullDelegate)
330                                throw new ParserException("event arrived without a delegate being specified");
331                }
332
333                public void startRecord() 
334                        throws ParserException 
335                { 
336                        if (delegate != null)
337                                delegate.startRecord();
338                        else if (exceptionOnNullDelegate)
339                                throw new ParserException("event arrived without a delegate being specified"); 
340                }
341
342                public void endRecord() 
343                        throws ParserException 
344                { 
345                        if (delegate != null)
346                                delegate.endRecord();
347                        else if (exceptionOnNullDelegate)
348                                throw new ParserException("event arrived without a delegate being specified"); 
349                }
350
351                public void value(TagValueContext ctxt, java.lang.Object value)
352                        throws ParserException
353                { 
354                        if (delegate != null)
355                                delegate.value(ctxt, value);
356                        else if (exceptionOnNullDelegate) 
357                                throw new ParserException("event arrived without a delegate being specified");
358                }
359        }
360
361
362        /**
363         * current state
364         */
365        private State statePointer;
366
367        /**
368         * mapping between state labels and states
369         */
370        private Map states = new HashMap();
371        private BasicState magicalState;
372
373        public StateMachine()
374        {
375                magicalState = createState(MAGICAL_STATE);
376                statePointer = magicalState;
377        }
378
379        public BasicState getMagicalState() { return magicalState; }
380
381        public BasicState createState(String label)
382        {
383                if (states.get(label)!= null) {
384                        throw new IllegalArgumentException("label " + label + " is not unique");
385                }
386
387                BasicState newState = new BasicState(label);
388                states.put(label, newState);
389
390                return newState;
391        }
392
393        public State getState(String label)
394        {
395                return (State) states.get(label);
396        }
397
398        public TransitionTable createTransitionTable()
399        {
400                return new TransitionTable();
401        }
402
403        /**
404         * TagValueWrapper interface
405         */
406        public void setDelegate(TagValueListener delegate) { this.delegate = delegate; }
407        public TagValueListener getDelegate() { return delegate; }
408
409        /**
410         * TagValueListener interface
411         */
412        public void startTag(Object tag)
413                throws ParserException
414        {
415                statePointer.transit(tag);
416
417                TagValueListener listener = statePointer.getListener();
418                if (listener != null) listener.startTag(tag);
419        }
420
421        public void endTag()
422                throws ParserException
423        {
424                TagValueListener lstnr;
425                if ((lstnr = ((BasicState) statePointer).getListener()) != null)
426                        lstnr.endTag();
427                statePointer.transit(END_TAG);
428        }
429
430        public void startRecord()
431                throws ParserException
432        {
433                TagValueListener lstnr;
434                if ((lstnr = ((BasicState) statePointer).getListener()) != null)
435                        lstnr.startRecord();
436                statePointer.transit(START_RECORD_TAG);
437        }
438
439        public void endRecord()
440                throws ParserException
441        {
442                TagValueListener lstnr;
443                if ((lstnr = ((BasicState) statePointer).getListener()) != null)
444                        lstnr.endRecord();
445                statePointer.transit(END_RECORD_TAG);
446        }
447
448        public void value(TagValueContext ctxt, Object value)
449                throws ParserException
450        {
451                TagValueListener lstnr;
452                if ((lstnr = ((BasicState) statePointer).getListener()) != null)
453                        lstnr.value(ctxt, value);
454        }
455}