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                            handledChannels.add(channelReplacements == null
270                                ? c
271                                : channelReplacements.getOrDefault(c, c));
272                        }
273                    }
274                }
275                if (handledChannels.isEmpty() || addDefaultChannel) {
276                    handledChannels.add(Components.manager(component)
277                        .channel().defaultCriterion());
278                }
279                // Finally, a component always handles events
280                // directed at it directly.
281                if (component instanceof Channel) {
282                    handledChannels.add(
283                        ((Channel) component).defaultCriterion());
284                }
285
286                try {
287                    // Get all paths from the annotation.
288                    if (!annotation.patterns()[0].isEmpty()) {
289                        for (String p : annotation.patterns()) {
290                            handledPatterns.add(new ResourcePattern(p));
291                        }
292                    }
293
294                    // Add additionally provided path
295                    if (pattern != null) {
296                        handledPatterns.add(new ResourcePattern(pattern));
297                    }
298                } catch (ParseException e) {
299                    throw new IllegalArgumentException(e.getMessage(), e);
300                }
301            }
302
303            @Override
304            @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
305                "PMD.NPathComplexity", "PMD.CognitiveComplexity" })
306            public boolean includes(Eligible event, Eligible[] channels) {
307                boolean match = false;
308                for (Object eventType : handledEventTypes) {
309                    if (Class.class.isInstance(eventType)
310                        && ((Class<?>) eventType)
311                            .isAssignableFrom(event.getClass())) {
312                        match = true;
313                        break;
314                    }
315
316                }
317                if (!match) {
318                    return false;
319                }
320                match = false;
321                // Try channels
322                for (Eligible channel : channels) {
323                    for (Object channelValue : handledChannels) {
324                        if (channel.isEligibleFor(channelValue)) {
325                            match = true;
326                            break;
327                        }
328                    }
329                }
330                if (!match) {
331                    return false;
332                }
333                if (!(event instanceof Request.In)) {
334                    return false;
335                }
336                for (ResourcePattern rp : handledPatterns) {
337                    if (((Request.In) event).isEligibleFor(
338                        Request.In.createMatchValue(Request.In.class, rp))) {
339                        return true;
340                    }
341                }
342                return false;
343            }
344
345            @Override
346            public boolean includes(EventBase<?> event) {
347                for (ResourcePattern rp : handledPatterns) {
348                    if (rp.matches(((Request.In) event).requestUri()) >= 0) {
349                        return true;
350                    }
351                }
352                return false;
353            }
354
355            /*
356             * (non-Javadoc)
357             * 
358             * @see java.lang.Object#toString()
359             */
360            @Override
361            public String toString() {
362                StringBuilder builder = new StringBuilder();
363                builder.append("Scope [");
364                if (handledEventTypes != null) {
365                    builder.append("handledEventTypes=")
366                        .append(handledEventTypes.stream().map(value -> {
367                            if (value instanceof Class) {
368                                return Components.className((Class<?>) value);
369                            }
370                            return value.toString();
371                        }).collect(Collectors.toSet()));
372                    builder.append(", ");
373                }
374                if (handledChannels != null) {
375                    builder.append("handledChannels=");
376                    builder.append(handledChannels);
377                    builder.append(", ");
378                }
379                if (handledPatterns != null) {
380                    builder.append("handledPatterns=");
381                    builder.append(handledPatterns);
382                }
383                builder.append(']');
384                return builder.toString();
385            }
386
387        }
388    }
389}