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.ComponentVertex;
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, 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        @SuppressWarnings("PMD.UnnecessaryBoxing")
166        public static void add(ComponentType component, String method,
167                String pattern, int priority) {
168            add(component, method, pattern, Integer.valueOf(priority));
169        }
170
171        @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
172            "PMD.CognitiveComplexity" })
173        private static void add(ComponentType component, String method,
174                String pattern, Integer priority) {
175            try {
176                for (Method m : component.getClass().getMethods()) {
177                    if (!m.getName().equals(method)) {
178                        continue;
179                    }
180                    for (Annotation annotation : m.getDeclaredAnnotations()) {
181                        Class<?> annoType = annotation.annotationType();
182                        HandlerDefinition hda
183                            = annoType.getAnnotation(HandlerDefinition.class);
184                        if (hda == null
185                            || !RequestHandler.class.isAssignableFrom(annoType)
186                            || !((RequestHandler) annotation).dynamic()) {
187                            continue;
188                        }
189                        @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
190                        Scope scope = new Scope(component, m,
191                            (RequestHandler) annotation,
192                            component instanceof ComponentVertex vertex
193                                ? vertex.channelReplacements()
194                                : Collections.emptyMap(),
195                            pattern);
196                        Components.manager(component)
197                            .addHandler(m, scope, priority == null
198                                ? ((RequestHandler) annotation).priority()
199                                : priority);
200                        return;
201                    }
202                }
203                throw new IllegalArgumentException(
204                    "No method named \"" + method + "\" with DynamicHandler"
205                        + " annotation and correct parameter list.");
206            } catch (SecurityException e) {
207                throw (RuntimeException) new IllegalArgumentException()
208                    .initCause(e);
209            }
210        }
211
212        /**
213         * The scope implementation.
214         */
215        public static class Scope implements HandlerScope, InvocationFilter {
216
217            private final Set<Object> handledEventTypes = new HashSet<>();
218            private final Set<Object> handledChannels = new HashSet<>();
219            private final Set<ResourcePattern> handledPatterns
220                = new HashSet<>();
221
222            /**
223             * Instantiates a new scope.
224             *
225             * @param component the component
226             * @param method the method
227             * @param annotation the annotation
228             * @param channelReplacements the channel replacements
229             * @param pattern the pattern
230             */
231            @SuppressWarnings({ "PMD.CyclomaticComplexity",
232                "PMD.NPathComplexity", "PMD.AvoidDeeplyNestedIfStmts",
233                "PMD.CollapsibleIfStatements", "PMD.CognitiveComplexity",
234                "PMD.AvoidInstantiatingObjectsInLoops", "PMD.NcssCount" })
235            public Scope(ComponentType component,
236                    Method method, RequestHandler annotation,
237                    Map<Class<? extends Channel>, Object[]> channelReplacements,
238                    String pattern) {
239                if (!HandlerDefinition.Evaluator.checkMethodSignature(method)) {
240                    throw new IllegalArgumentException("Method "
241                        + method.toString() + " cannot be used as"
242                        + " handler (wrong signature).");
243                }
244                // Get all event keys from the handler annotation.
245                if (annotation.events()[0] != NoEvent.class) {
246                    handledEventTypes
247                        .addAll(Arrays.asList(annotation.events()));
248                }
249                // If no event types are given, try first parameter.
250                if (handledEventTypes.isEmpty()) {
251                    Class<?>[] paramTypes = method.getParameterTypes();
252                    if (paramTypes.length > 0) {
253                        if (Event.class.isAssignableFrom(paramTypes[0])) {
254                            handledEventTypes.add(paramTypes[0]);
255                        }
256                    }
257                }
258
259                // Get channel keys from the annotation.
260                boolean addDefaultChannel = false;
261                if (annotation.channels()[0] != NoChannel.class) {
262                    for (Class<?> c : annotation.channels()) {
263                        if (c == Self.class) {
264                            if (!(component instanceof Channel)) {
265                                throw new IllegalArgumentException(
266                                    "Canot use channel This.class in "
267                                        + "annotation of " + method
268                                        + " because " + getClass().getName()
269                                        + " does not implement Channel.");
270                            }
271                            // Will be added anyway, see below
272                        } else if (c == Channel.Default.class) {
273                            addDefaultChannel = true;
274                        } else {
275                            if (channelReplacements != null
276                                && channelReplacements.containsKey(c)) {
277                                handledChannels.addAll(
278                                    Arrays.asList(channelReplacements.get(c)));
279                            } else {
280                                handledChannels.add(c);
281                            }
282                        }
283                    }
284                }
285                if (handledChannels.isEmpty() || addDefaultChannel) {
286                    handledChannels.add(Components.manager(component)
287                        .channel().defaultCriterion());
288                }
289                // Finally, a component always handles events
290                // directed at it directly.
291                if (component instanceof Channel) {
292                    handledChannels.add(
293                        ((Channel) component).defaultCriterion());
294                }
295
296                try {
297                    // Get all paths from the annotation.
298                    if (!annotation.patterns()[0].isEmpty()) {
299                        for (String p : annotation.patterns()) {
300                            handledPatterns.add(new ResourcePattern(p));
301                        }
302                    }
303
304                    // Add additionally provided path
305                    if (pattern != null) {
306                        handledPatterns.add(new ResourcePattern(pattern));
307                    }
308                } catch (ParseException e) {
309                    throw new IllegalArgumentException(e.getMessage(), e);
310                }
311            }
312
313            @Override
314            @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
315                "PMD.NPathComplexity", "PMD.CognitiveComplexity" })
316            public boolean includes(Eligible event, Eligible[] channels) {
317                boolean match = false;
318                for (Object eventType : handledEventTypes) {
319                    if (Class.class.isInstance(eventType)
320                        && ((Class<?>) eventType)
321                            .isAssignableFrom(event.getClass())) {
322                        match = true;
323                        break;
324                    }
325
326                }
327                if (!match) {
328                    return false;
329                }
330                match = false;
331                // Try channels
332                for (Eligible channel : channels) {
333                    for (Object channelValue : handledChannels) {
334                        if (channel.isEligibleFor(channelValue)) {
335                            match = true;
336                            break;
337                        }
338                    }
339                }
340                if (!match) {
341                    return false;
342                }
343                if (!(event instanceof Request.In)) {
344                    return false;
345                }
346                for (ResourcePattern rp : handledPatterns) {
347                    if (((Request.In) event).isEligibleFor(
348                        Request.In.createMatchValue(Request.In.class, rp))) {
349                        return true;
350                    }
351                }
352                return false;
353            }
354
355            @Override
356            public boolean includes(EventBase<?> event) {
357                for (ResourcePattern rp : handledPatterns) {
358                    if (rp.matches(((Request.In) event).requestUri()) >= 0) {
359                        return true;
360                    }
361                }
362                return false;
363            }
364
365            /*
366             * (non-Javadoc)
367             * 
368             * @see java.lang.Object#toString()
369             */
370            @Override
371            public String toString() {
372                StringBuilder builder = new StringBuilder(100);
373                builder.append("Scope [");
374                if (handledEventTypes != null) {
375                    builder.append("handledEventTypes=")
376                        .append(handledEventTypes.stream().map(value -> {
377                            if (value instanceof Class) {
378                                return Components.className((Class<?>) value);
379                            }
380                            return value.toString();
381                        }).collect(Collectors.toSet())).append(", ");
382                }
383                if (handledChannels != null) {
384                    builder.append("handledChannels=").append(handledChannels)
385                        .append(", ");
386                }
387                if (handledPatterns != null) {
388                    builder.append("handledPatterns=").append(handledPatterns);
389                }
390                builder.append(']');
391                return builder.toString();
392            }
393
394        }
395    }
396}