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}