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}