001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 *
019 */
020package org.apache.mina.statemachine;
021
022import java.lang.annotation.Annotation;
023import java.lang.reflect.Field;
024import java.lang.reflect.Method;
025import java.lang.reflect.Modifier;
026import java.util.ArrayList;
027import java.util.Arrays;
028import java.util.Comparator;
029import java.util.HashMap;
030import java.util.LinkedHashMap;
031import java.util.LinkedList;
032import java.util.List;
033import java.util.Map;
034
035import org.apache.mina.statemachine.annotation.OnEntry;
036import org.apache.mina.statemachine.annotation.OnExit;
037import org.apache.mina.statemachine.annotation.Transition;
038import org.apache.mina.statemachine.annotation.TransitionAnnotation;
039import org.apache.mina.statemachine.annotation.Transitions;
040import org.apache.mina.statemachine.event.Event;
041import org.apache.mina.statemachine.transition.MethodSelfTransition;
042import org.apache.mina.statemachine.transition.MethodTransition;
043import org.apache.mina.statemachine.transition.SelfTransition;
044
045/**
046 * Creates {@link StateMachine}s by reading {@link org.apache.mina.statemachine.annotation.State},
047 * {@link Transition} and {@link Transitions} (or equivalent) and {@link SelfTransition} annotations from one or more arbitrary
048 * objects.
049 * 
050 *
051 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
052 */
053public class StateMachineFactory {
054    private final Class<? extends Annotation> transitionAnnotation;
055
056    private final Class<? extends Annotation> transitionsAnnotation;
057
058    private final Class<? extends Annotation> entrySelfTransitionsAnnotation;
059
060    private final Class<? extends Annotation> exitSelfTransitionsAnnotation;
061
062    protected StateMachineFactory(Class<? extends Annotation> transitionAnnotation,
063            Class<? extends Annotation> transitionsAnnotation,
064            Class<? extends Annotation> entrySelfTransitionsAnnotation,
065            Class<? extends Annotation> exitSelfTransitionsAnnotation) {
066        this.transitionAnnotation = transitionAnnotation;
067        this.transitionsAnnotation = transitionsAnnotation;
068        this.entrySelfTransitionsAnnotation = entrySelfTransitionsAnnotation;
069        this.exitSelfTransitionsAnnotation = exitSelfTransitionsAnnotation;
070    }
071
072    /**
073     * Returns a new {@link StateMachineFactory} instance which creates
074     * {@link StateMachine}s by reading the specified {@link Transition}
075     * equivalent annotation.
076     * 
077     * @param transitionAnnotation the {@link Transition} equivalent annotation.
078     * @return the {@link StateMachineFactory}.
079     */
080    public static StateMachineFactory getInstance(Class<? extends Annotation> transitionAnnotation) {
081        TransitionAnnotation a = transitionAnnotation.getAnnotation(TransitionAnnotation.class);
082        if (a == null) {
083            throw new IllegalArgumentException("The annotation class " + transitionAnnotation
084                    + " has not been annotated with the " + TransitionAnnotation.class.getName() + " annotation");
085        }
086        return new StateMachineFactory(transitionAnnotation, a.value(), OnEntry.class, OnExit.class);
087
088    }
089
090    /**
091     * Creates a new {@link StateMachine} from the specified handler object and
092     * using a start state with id <code>start</code>.
093     * 
094     * @param handler the object containing the annotations describing the
095     *        state machine.
096     * @return the {@link StateMachine} object.
097     */
098    public StateMachine create(Object handler) {
099        return create(handler, new Object[0]);
100    }
101
102    /**
103     * Creates a new {@link StateMachine} from the specified handler object and
104     * using the {@link State} with the specified id as start state.
105     * 
106     * @param start the id of the start {@link State} to use.
107     * @param handler the object containing the annotations describing the
108     *        state machine.
109     * @return the {@link StateMachine} object.
110     */
111    public StateMachine create(String start, Object handler) {
112        return create(start, handler, new Object[0]);
113    }
114
115    /**
116     * Creates a new {@link StateMachine} from the specified handler objects and
117     * using a start state with id <code>start</code>.
118     * 
119     * @param handler the first object containing the annotations describing the
120     *        state machine.
121     * @param handlers zero or more additional objects containing the
122     *        annotations describing the state machine.
123     * @return the {@link StateMachine} object.
124     */
125    public StateMachine create(Object handler, Object... handlers) {
126        return create("start", handler, handlers);
127    }
128
129    /**
130     * Creates a new {@link StateMachine} from the specified handler objects and
131     * using the {@link State} with the specified id as start state.
132     * 
133     * @param start the id of the start {@link State} to use.
134     * @param handler the first object containing the annotations describing the
135     *        state machine.
136     * @param handlers zero or more additional objects containing the
137     *        annotations describing the state machine.
138     * @return the {@link StateMachine} object.
139     */
140    public StateMachine create(String start, Object handler, Object... handlers) {
141
142        Map<String, State> states = new HashMap<String, State>();
143        List<Object> handlersList = new ArrayList<Object>(1 + handlers.length);
144        handlersList.add(handler);
145        handlersList.addAll(Arrays.asList(handlers));
146
147        LinkedList<Field> fields = new LinkedList<Field>();
148        for (Object h : handlersList) {
149            fields.addAll(getFields(h instanceof Class ? (Class<?>) h : h.getClass()));
150        }
151        for (State state : createStates(fields)) {
152            states.put(state.getId(), state);
153        }
154
155        if (!states.containsKey(start)) {
156            throw new StateMachineCreationException("Start state '" + start + "' not found.");
157        }
158
159        setupTransitions(transitionAnnotation, transitionsAnnotation, entrySelfTransitionsAnnotation,
160                exitSelfTransitionsAnnotation, states, handlersList);
161
162        return new StateMachine(states.values(), start);
163    }
164
165    private static void setupTransitions(Class<? extends Annotation> transitionAnnotation,
166            Class<? extends Annotation> transitionsAnnotation,
167            Class<? extends Annotation> onEntrySelfTransitionAnnotation,
168            Class<? extends Annotation> onExitSelfTransitionAnnotation, Map<String, State> states, List<Object> handlers) {
169        for (Object handler : handlers) {
170            setupTransitions(transitionAnnotation, transitionsAnnotation, onEntrySelfTransitionAnnotation,
171                    onExitSelfTransitionAnnotation, states, handler);
172        }
173    }
174
175    private static void setupSelfTransitions(Method m, Class<? extends Annotation> onEntrySelfTransitionAnnotation,
176            Class<? extends Annotation> onExitSelfTransitionAnnotation, Map<String, State> states, Object handler) {
177        if (m.isAnnotationPresent(OnEntry.class)) {
178            OnEntry onEntryAnnotation = (OnEntry) m.getAnnotation(onEntrySelfTransitionAnnotation);
179            State state = states.get(onEntryAnnotation.value());
180            if (state == null) {
181                throw new StateMachineCreationException("Error encountered "
182                        + "when processing onEntry annotation in method " + m + ". state " + onEntryAnnotation.value()
183                        + " not Found.");
184
185            }
186            state.addOnEntrySelfTransaction(new MethodSelfTransition(m, handler));
187        }
188
189        if (m.isAnnotationPresent(OnExit.class)) {
190            OnExit onExitAnnotation = (OnExit) m.getAnnotation(onExitSelfTransitionAnnotation);
191            State state = states.get(onExitAnnotation.value());
192            if (state == null) {
193                throw new StateMachineCreationException("Error encountered "
194                        + "when processing onExit annotation in method " + m + ". state " + onExitAnnotation.value()
195                        + " not Found.");
196
197            }
198            state.addOnExitSelfTransaction(new MethodSelfTransition(m, handler));
199        }
200
201    }
202
203    private static void setupTransitions(Class<? extends Annotation> transitionAnnotation,
204            Class<? extends Annotation> transitionsAnnotation,
205            Class<? extends Annotation> onEntrySelfTransitionAnnotation,
206            Class<? extends Annotation> onExitSelfTransitionAnnotation, Map<String, State> states, Object handler) {
207
208        Method[] methods = handler.getClass().getDeclaredMethods();
209        Arrays.sort(methods, new Comparator<Method>() {
210            public int compare(Method m1, Method m2) {
211                return m1.toString().compareTo(m2.toString());
212            }
213        });
214
215        for (Method m : methods) {
216            setupSelfTransitions(m, onEntrySelfTransitionAnnotation, onExitSelfTransitionAnnotation, states, handler);
217
218            List<TransitionWrapper> transitionAnnotations = new ArrayList<TransitionWrapper>();
219            if (m.isAnnotationPresent(transitionAnnotation)) {
220                transitionAnnotations.add(new TransitionWrapper(transitionAnnotation, m
221                        .getAnnotation(transitionAnnotation)));
222            }
223            if (m.isAnnotationPresent(transitionsAnnotation)) {
224                transitionAnnotations.addAll(Arrays.asList(new TransitionsWrapper(transitionAnnotation,
225                        transitionsAnnotation, m.getAnnotation(transitionsAnnotation)).value()));
226            }
227
228            if (transitionAnnotations.isEmpty()) {
229                continue;
230            }
231
232            for (TransitionWrapper annotation : transitionAnnotations) {
233                Object[] eventIds = annotation.on();
234                if (eventIds.length == 0) {
235                    throw new StateMachineCreationException("Error encountered " + "when processing method " + m
236                            + ". No event ids specified.");
237                }
238                if (annotation.in().length == 0) {
239                    throw new StateMachineCreationException("Error encountered " + "when processing method " + m
240                            + ". No states specified.");
241                }
242
243                State next = null;
244                if (!annotation.next().equals(Transition.SELF)) {
245                    next = states.get(annotation.next());
246                    if (next == null) {
247                        throw new StateMachineCreationException("Error encountered " + "when processing method " + m
248                                + ". Unknown next state: " + annotation.next() + ".");
249                    }
250                }
251
252                for (Object event : eventIds) {
253                    if (event == null) {
254                        event = Event.WILDCARD_EVENT_ID;
255                    }
256                    if (!(event instanceof String)) {
257                        event = event.toString();
258                    }
259                    for (String in : annotation.in()) {
260                        State state = states.get(in);
261                        if (state == null) {
262                            throw new StateMachineCreationException("Error encountered " + "when processing method "
263                                    + m + ". Unknown state: " + in + ".");
264                        }
265
266                        state.addTransition(new MethodTransition(event, next, m, handler), annotation.weight());
267                    }
268                }
269            }
270        }
271    }
272
273    static List<Field> getFields(Class<?> clazz) {
274        LinkedList<Field> fields = new LinkedList<Field>();
275
276        for (Field f : clazz.getDeclaredFields()) {
277            if (!f.isAnnotationPresent(org.apache.mina.statemachine.annotation.State.class)) {
278                continue;
279            }
280
281            if ((f.getModifiers() & Modifier.STATIC) == 0 || (f.getModifiers() & Modifier.FINAL) == 0
282                    || !f.getType().equals(String.class)) {
283                throw new StateMachineCreationException("Error encountered when " + "processing field " + f
284                        + ". Only static final " + "String fields can be used with the @State " + "annotation.");
285            }
286
287            if (!f.isAccessible()) {
288                f.setAccessible(true);
289            }
290
291            fields.add(f);
292        }
293
294        return fields;
295    }
296
297    static State[] createStates(List<Field> fields) {
298        LinkedHashMap<String, State> states = new LinkedHashMap<String, State>();
299
300        while (!fields.isEmpty()) {
301            int size = fields.size();
302            int numStates = states.size();
303            for (int i = 0; i < size; i++) {
304                Field f = fields.remove(0);
305
306                String value = null;
307                try {
308                    value = (String) f.get(null);
309                } catch (IllegalAccessException iae) {
310                    throw new StateMachineCreationException("Error encountered when " + "processing field " + f + ".",
311                            iae);
312                }
313
314                org.apache.mina.statemachine.annotation.State stateAnnotation = f
315                        .getAnnotation(org.apache.mina.statemachine.annotation.State.class);
316                if (stateAnnotation.value().equals(org.apache.mina.statemachine.annotation.State.ROOT)) {
317                    states.put(value, new State(value));
318                } else if (states.containsKey(stateAnnotation.value())) {
319                    states.put(value, new State(value, states.get(stateAnnotation.value())));
320                } else {
321                    // Move to the back of the list of fields for later
322                    // processing
323                    fields.add(f);
324                }
325            }
326
327            /*
328             * If no new states were added to states during this iteration it
329             * means that all fields in fields specify non-existent parents.
330             */
331            if (states.size() == numStates) {
332                throw new StateMachineCreationException("Error encountered while creating "
333                        + "FSM. The following fields specify non-existing " + "parent states: " + fields);
334            }
335        }
336
337        return states.values().toArray(new State[0]);
338    }
339
340    private static class TransitionWrapper {
341        private final Class<? extends Annotation> transitionClazz;
342
343        private final Annotation annotation;
344
345        public TransitionWrapper(Class<? extends Annotation> transitionClazz, Annotation annotation) {
346            this.transitionClazz = transitionClazz;
347            this.annotation = annotation;
348        }
349
350        Object[] on() {
351            return getParameter("on", Object[].class);
352        }
353
354        String[] in() {
355            return getParameter("in", String[].class);
356        }
357
358        String next() {
359            return getParameter("next", String.class);
360        }
361
362        int weight() {
363            return getParameter("weight", Integer.TYPE);
364        }
365
366        @SuppressWarnings("unchecked")
367        private <T> T getParameter(String name, Class<T> returnType) {
368            try {
369                Method m = transitionClazz.getMethod(name);
370                if (!returnType.isAssignableFrom(m.getReturnType())) {
371                    throw new NoSuchMethodException();
372                }
373                return (T) m.invoke(annotation);
374            } catch (Exception e) {
375                throw new StateMachineCreationException("Could not get parameter '" + name
376                        + "' from Transition annotation " + transitionClazz);
377            }
378        }
379    }
380
381    private static class TransitionsWrapper {
382        private final Class<? extends Annotation> transitionsclazz;
383
384        private final Class<? extends Annotation> transitionClazz;
385
386        private final Annotation annotation;
387
388        public TransitionsWrapper(Class<? extends Annotation> transitionClazz,
389                Class<? extends Annotation> transitionsclazz, Annotation annotation) {
390            this.transitionClazz = transitionClazz;
391            this.transitionsclazz = transitionsclazz;
392            this.annotation = annotation;
393        }
394
395        TransitionWrapper[] value() {
396            Annotation[] annos = getParameter("value", Annotation[].class);
397            TransitionWrapper[] wrappers = new TransitionWrapper[annos.length];
398            for (int i = 0; i < annos.length; i++) {
399                wrappers[i] = new TransitionWrapper(transitionClazz, annos[i]);
400            }
401            return wrappers;
402        }
403
404        @SuppressWarnings("unchecked")
405        private <T> T getParameter(String name, Class<T> returnType) {
406            try {
407                Method m = transitionsclazz.getMethod(name);
408                if (!returnType.isAssignableFrom(m.getReturnType())) {
409                    throw new NoSuchMethodException();
410                }
411                return (T) m.invoke(annotation);
412            } catch (Exception e) {
413                throw new StateMachineCreationException("Could not get parameter '" + name
414                        + "' from Transitions annotation " + transitionsclazz);
415            }
416        }
417    }
418}