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