001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2016-2018 Michael N. Lipp
004 * 
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 * 
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
013 * for more details.
014 * 
015 * You should have received a copy of the GNU Affero General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jgrapes.http.annotation;
020
021import java.lang.annotation.Annotation;
022import java.lang.annotation.Documented;
023import java.lang.annotation.ElementType;
024import java.lang.annotation.Retention;
025import java.lang.annotation.RetentionPolicy;
026import java.lang.annotation.Target;
027import java.lang.reflect.Method;
028import java.text.ParseException;
029import java.util.Arrays;
030import java.util.Collections;
031import java.util.HashSet;
032import java.util.Map;
033import java.util.Set;
034import java.util.stream.Collectors;
035import org.jgrapes.core.Channel;
036import org.jgrapes.core.ComponentType;
037import org.jgrapes.core.Components;
038import org.jgrapes.core.Eligible;
039import org.jgrapes.core.Event;
040import org.jgrapes.core.HandlerScope;
041import org.jgrapes.core.InvocationFilter;
042import org.jgrapes.core.Self;
043import org.jgrapes.core.annotation.Handler.NoChannel;
044import org.jgrapes.core.annotation.Handler.NoEvent;
045import org.jgrapes.core.annotation.HandlerDefinition;
046import org.jgrapes.core.annotation.HandlerDefinition.ChannelReplacements;
047import org.jgrapes.core.internal.EventBase;
048import org.jgrapes.http.ResourcePattern;
049import org.jgrapes.http.events.Request;
050import org.jgrapes.http.events.Request.In;
051
052/**
053 * This annotation marks a method as handler for events. The method is 
054 * invoked for events that have a type derived from {@link Request} and
055 * are matched by one of the specified {@link ResourcePattern}s.
056 * 
057 * Note that matching uses a shortened request URI for reasons outlined
058 * in {@link In#defaultCriterion()}. Specifying patterns with
059 * more path components than are used by the event's default criterion
060 * may therefore result in unexpected events. If the default criterion
061 * of an event shortens a requested URI "/foo/bar" to "/foo/**" and
062 * the {@link RequestHandler} annotation specifies "/foo/baz" as pattern,
063 * the handler will be invoked.
064 */
065@Documented
066@Retention(RetentionPolicy.RUNTIME)
067@Target(ElementType.METHOD)
068@HandlerDefinition(evaluator = RequestHandler.Evaluator.class)
069public @interface RequestHandler {
070
071    /**
072     * Specifies classes of events that the handler is to receive.
073     * 
074     * @return the event classes
075     */
076    @SuppressWarnings("rawtypes")
077    Class<? extends Event>[] events() default NoEvent.class;
078
079    /**
080     * Specifies classes of channels that the handler listens on.
081     * 
082     * @return the channel classes
083     */
084    Class<? extends Channel>[] channels() default NoChannel.class;
085
086    /**
087     * Specifies the patterns that the handler is supposed to handle
088     * (see {@link ResourcePattern}).
089     * 
090     * @return the patterns
091     */
092    String[] patterns() default "";
093
094    /**
095     * Specifies a priority. The value is used to sort handlers.
096     * Handlers with higher priority are invoked first.
097     * 
098     * @return the priority
099     */
100    int priority() default 0;
101
102    /**
103     * Returns {@code true} if the annotated annotation defines a
104     * dynamic handler. A dynamic handler must be added to the set of
105     * handlers of a component explicitly.
106     * 
107     * @return the result
108     */
109    boolean dynamic() default false;
110
111    /**
112     * This class provides the {@link Evaluator} for the {@link RequestHandler}
113     * annotation. It implements the behavior as described for the annotation.
114     */
115    class Evaluator implements HandlerDefinition.Evaluator {
116
117        /*
118         * (non-Javadoc)
119         * 
120         * @see
121         * org.jgrapes.core.annotation.HandlerDefinition.Evaluator#getPriority()
122         */
123        @Override
124        public int priority(Annotation annotation) {
125            return ((RequestHandler) annotation).priority();
126        }
127
128        @Override
129        public HandlerScope scope(ComponentType component, Method method,
130                ChannelReplacements channelReplacements) {
131            RequestHandler annotation
132                = method.getAnnotation(RequestHandler.class);
133            if (annotation.dynamic()) {
134                return null;
135            }
136            return new Scope(component, method, (RequestHandler) annotation,
137                channelReplacements, null);
138        }
139
140        /**
141         * Adds the given method of the given component as a 
142         * dynamic handler for a specific pattern. Other informations
143         * are taken from the annotation.
144         * 
145         * @param component the component
146         * @param method the name of the method that implements the handler
147         * @param pattern the pattern
148         */
149        public static void add(ComponentType component, String method,
150                String pattern) {
151            add(component, method, pattern, null);
152        }
153
154        /**
155         * Adds the given method of the given component as a 
156         * dynamic handler for a specific pattern with the specified
157         * priority. Other informations are taken from the annotation.
158         *
159         * @param component the component
160         * @param method the name of the method that implements the handler
161         * @param pattern the pattern
162         * @param priority the priority
163         */
164        public static void add(ComponentType component, String method,
165                String pattern, int priority) {
166            add(component, method, pattern, Integer.valueOf(priority));
167        }
168
169        @SuppressWarnings("PMD.AvoidBranchingStatementAsLastInLoop")
170        private static void add(ComponentType component, String method,
171                String pattern, Integer priority) {
172            try {
173                for (Method m : component.getClass().getMethods()) {
174                    if (!m.getName().equals(method)) {
175                        continue;
176                    }
177                    for (Annotation annotation : m.getDeclaredAnnotations()) {
178                        Class<?> annoType = annotation.annotationType();
179                        HandlerDefinition hda
180                            = annoType.getAnnotation(HandlerDefinition.class);
181                        if (hda == null
182                            || !RequestHandler.class.isAssignableFrom(annoType)
183                            || !((RequestHandler) annotation).dynamic()) {
184                            continue;
185                        }
186                        @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
187                        Scope scope = new Scope(component, m,
188                            (RequestHandler) annotation,
189                            Collections.emptyMap(), pattern);
190                        Components.manager(component)
191                            .addHandler(m, scope, priority == null
192                                ? ((RequestHandler) annotation).priority()
193                                : priority);
194                        return;
195                    }
196                }
197                throw new IllegalArgumentException(
198                    "No method named \"" + method + "\" with DynamicHandler"
199                        + " annotation and correct parameter list.");
200            } catch (SecurityException e) {
201                throw (RuntimeException) (new IllegalArgumentException()
202                    .initCause(e));
203            }
204        }
205
206        /**
207         * The scope implementation.
208         */
209        public static class Scope implements HandlerScope, InvocationFilter {
210
211            private final Set<Object> handledEventTypes = new HashSet<>();
212            private final Set<Object> handledChannels = new HashSet<>();
213            private final Set<ResourcePattern> handledPatterns
214                = new HashSet<>();
215
216            /**
217             * Instantiates a new scope.
218             *
219             * @param component the component
220             * @param method the method
221             * @param annotation the annotation
222             * @param channelReplacements the channel replacements
223             * @param pattern the pattern
224             */
225            @SuppressWarnings({ "PMD.CyclomaticComplexity",
226                "PMD.NPathComplexity", "PMD.AvoidDeeplyNestedIfStmts",
227                "PMD.CollapsibleIfStatements", "PMD.CognitiveComplexity",
228                "PMD.AvoidInstantiatingObjectsInLoops" })
229            public Scope(ComponentType component,
230                    Method method, RequestHandler annotation,
231                    Map<Class<? extends Channel>, Object[]> channelReplacements,
232                    String pattern) {
233                if (!HandlerDefinition.Evaluator.checkMethodSignature(method)) {
234                    throw new IllegalArgumentException("Method "
235                        + method.toString() + " cannot be used as"
236                        + " handler (wrong signature).");
237                }
238                // Get all event keys from the handler annotation.
239                if (annotation.events()[0] != NoEvent.class) {
240                    handledEventTypes
241                        .addAll(Arrays.asList(annotation.events()));
242                }
243                // If no event types are given, try first parameter.
244                if (handledEventTypes.isEmpty()) {
245                    Class<?>[] paramTypes = method.getParameterTypes();
246                    if (paramTypes.length > 0) {
247                        if (Event.class.isAssignableFrom(paramTypes[0])) {
248                            handledEventTypes.add(paramTypes[0]);
249                        }
250                    }
251                }
252
253                // Get channel keys from the annotation.
254                boolean addDefaultChannel = false;
255                if (annotation.channels()[0] != NoChannel.class) {
256                    for (Class<?> c : annotation.channels()) {
257                        if (c == Self.class) {
258                            if (!(component instanceof Channel)) {
259                                throw new IllegalArgumentException(
260                                    "Canot use channel This.class in "
261                                        + "annotation of " + method
262                                        + " because " + getClass().getName()
263                                        + " does not implement Channel.");
264                            }
265                            // Will be added anyway, see below
266                        } else if (c == Channel.Default.class) {
267                            addDefaultChannel = true;
268                        } else {
269                            if (channelReplacements != null
270                                && channelReplacements.containsKey(c)) {
271                                handledChannels.addAll(
272                                    Arrays.asList(channelReplacements.get(c)));
273                            } else {
274                                handledChannels.add(c);
275                            }
276                        }
277                    }
278                }
279                if (handledChannels.isEmpty() || addDefaultChannel) {
280                    handledChannels.add(Components.manager(component)
281                        .channel().defaultCriterion());
282                }
283                // Finally, a component always handles events
284                // directed at it directly.
285                if (component instanceof Channel) {
286                    handledChannels.add(
287                        ((Channel) component).defaultCriterion());
288                }
289
290                try {
291                    // Get all paths from the annotation.
292                    if (!annotation.patterns()[0].isEmpty()) {
293                        for (String p : annotation.patterns()) {
294                            handledPatterns.add(new ResourcePattern(p));
295                        }
296                    }
297
298                    // Add additionally provided path
299                    if (pattern != null) {
300                        handledPatterns.add(new ResourcePattern(pattern));
301                    }
302                } catch (ParseException e) {
303                    throw new IllegalArgumentException(e.getMessage(), e);
304                }
305            }
306
307            @Override
308            @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
309                "PMD.NPathComplexity", "PMD.CognitiveComplexity" })
310            public boolean includes(Eligible event, Eligible[] channels) {
311                boolean match = false;
312                for (Object eventType : handledEventTypes) {
313                    if (Class.class.isInstance(eventType)
314                        && ((Class<?>) eventType)
315                            .isAssignableFrom(event.getClass())) {
316                        match = true;
317                        break;
318                    }
319
320                }
321                if (!match) {
322                    return false;
323                }
324                match = false;
325                // Try channels
326                for (Eligible channel : channels) {
327                    for (Object channelValue : handledChannels) {
328                        if (channel.isEligibleFor(channelValue)) {
329                            match = true;
330                            break;
331                        }
332                    }
333                }
334                if (!match) {
335                    return false;
336                }
337                if (!(event instanceof Request.In)) {
338                    return false;
339                }
340                for (ResourcePattern rp : handledPatterns) {
341                    if (((Request.In) event).isEligibleFor(
342                        Request.In.createMatchValue(Request.In.class, rp))) {
343                        return true;
344                    }
345                }
346                return false;
347            }
348
349            @Override
350            public boolean includes(EventBase<?> event) {
351                for (ResourcePattern rp : handledPatterns) {
352                    if (rp.matches(((Request.In) event).requestUri()) >= 0) {
353                        return true;
354                    }
355                }
356                return false;
357            }
358
359            /*
360             * (non-Javadoc)
361             * 
362             * @see java.lang.Object#toString()
363             */
364            @Override
365            public String toString() {
366                StringBuilder builder = new StringBuilder();
367                builder.append("Scope [");
368                if (handledEventTypes != null) {
369                    builder.append("handledEventTypes=")
370                        .append(handledEventTypes.stream().map(value -> {
371                            if (value instanceof Class) {
372                                return Components.className((Class<?>) value);
373                            }
374                            return value.toString();
375                        }).collect(Collectors.toSet()));
376                    builder.append(", ");
377                }
378                if (handledChannels != null) {
379                    builder.append("handledChannels=");
380                    builder.append(handledChannels);
381                    builder.append(", ");
382                }
383                if (handledPatterns != null) {
384                    builder.append("handledPatterns=");
385                    builder.append(handledPatterns);
386                }
387                builder.append(']');
388                return builder.toString();
389            }
390
391        }
392    }
393}