Skip to content

Commit

Permalink
Added functionality to use a SkipInertia annotation for controllers a…
Browse files Browse the repository at this point in the history
…nd/or actions that should not be handled by the InertiaInterceptor.
  • Loading branch information
matrei committed Feb 22, 2023
1 parent 827a6e0 commit ec9e3c3
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 11 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#Thu, 09 Feb 2023 08:09:33 +0000
version=1.1.1-SNAPSHOT
version=1.2.0-SNAPSHOT
grailsVersion=5.3.2
grailsGradlePluginVersion=5.3.0
groovyVersion=3.0.14
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
package grails.plugin.inertia

import grails.config.Config
import grails.core.GrailsApplication
import grails.core.support.GrailsApplicationAware
import grails.core.support.GrailsConfigurationAware
import grails.plugin.inertia.annotation.AnnotationExcluder
import grails.plugin.inertia.annotation.SkipInertia
import grails.util.Environment
import grails.util.Holders
import groovy.json.JsonSlurper
import groovy.transform.CompileStatic
import io.micronaut.http.HttpStatus

import static grails.web.http.HttpHeaders.VARY
import static javax.servlet.http.HttpServletResponse.SC_CONFLICT
import static javax.servlet.http.HttpServletResponse.SC_MOVED_TEMPORARILY
import static javax.servlet.http.HttpServletResponse.SC_SEE_OTHER
import static Inertia.INERTIA_ATTRIBUTE_VERSION
import static Inertia.INERTIA_ATTRIBUTE_MANIFEST
import static Inertia.INERTIA_ATTRIBUTE_VERSION
import static grails.web.http.HttpHeaders.VARY

@CompileStatic
class InertiaInterceptor implements GrailsConfigurationAware {
class InertiaInterceptor implements GrailsConfigurationAware, GrailsApplicationAware {

String manifestLocation
String manifestHash = 'not yet calculated'
Expand All @@ -28,7 +30,14 @@ class InertiaInterceptor implements GrailsConfigurationAware {
private static final String CONTENT_TYPE_JSON = 'application/json;charset=utf-8'
private static final String CONTENT_TYPE_HTML = 'text/html;charset=utf-8'

InertiaInterceptor() { match controller: '*' }
InertiaInterceptor() {
match controller: '*'
}

@Override
void setGrailsApplication(GrailsApplication grailsApplication) {
AnnotationExcluder.excludeAnnotations(this, grailsApplication, SkipInertia)
}

boolean before() {

Expand All @@ -42,7 +51,7 @@ class InertiaInterceptor implements GrailsConfigurationAware {
if(isInertiaRequest && isGetRequest && manifestShouldBeUsed && isAssetsOutOfDate) {
log.debug 'Inertia asset version has changed, notifying Inertia client and aborting request processing to force full inertiaPage reload!'
header Inertia.INERTIA_HEADER_LOCATION, webRequest.currentRequest.forwardURI
render status: SC_CONFLICT
render status: HttpStatus.CONFLICT.code
return false
}

Expand All @@ -53,7 +62,7 @@ class InertiaInterceptor implements GrailsConfigurationAware {

// Changes the status code during redirects, ensuring they are made as
// GET requests, preventing "MethodNotAllowedHttpException" errors.
if (methodNotAllowedShouldBePrevented) response.status = SC_SEE_OTHER
if (methodNotAllowedShouldBePrevented) response.status = HttpStatus.SEE_OTHER.code

// Add the Javascript Manifest when not in Development Environment
// In Development Environment a node server should be started to serve the javascript files (npm run serve)
Expand Down Expand Up @@ -86,7 +95,7 @@ class InertiaInterceptor implements GrailsConfigurationAware {

boolean getManifestShouldBeUsed() { Environment.current != Environment.DEVELOPMENT }
boolean getIsGetRequest() { 'GET' == request.method }
boolean getMethodNotAllowedShouldBePrevented() { isInertiaRequest && response.status == SC_MOVED_TEMPORARILY && request.method in ['PUT', 'PATCH', 'DELETE'] }
boolean getMethodNotAllowedShouldBePrevented() { isInertiaRequest && response.status == HttpStatus.FOUND.code && request.method in ['PUT', 'PATCH', 'DELETE'] }
boolean getIsInertiaHtmlView() { modelAndView?.viewName == Inertia.INERTIA_VIEW_HTML }
boolean getIsInertiaRequest() { request.getHeader(INERTIA_HEADER_NAME) == INERTIA_HEADER_VALUE }
boolean getIsAssetsCurrent() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package grails.plugin.inertia.annotation;

import grails.artefact.Interceptor;
import grails.core.GrailsApplication;
import grails.core.GrailsClass;
import grails.core.GrailsControllerClass;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Objects;

public class AnnotationExcluder {

private static final Logger log = LoggerFactory.getLogger(AnnotationExcluder.class);

/**
* Excludes all Controllers and/or Actions that have the provided annotation applied to them from
* triggering the provided interceptor.
* @param interceptor The interceptor to exclude the matches for
* @param grailsApplication The current grails application
* @param annotation The annotation to search for
*/
public static void excludeAnnotations(final Interceptor interceptor, final GrailsApplication grailsApplication, final Class<? extends Annotation> annotation) {
final GrailsClass[] controllers = grailsApplication.getArtefacts("Controller");

for (GrailsClass controller : controllers) {

final String controllerName = controller.getLogicalPropertyName();
final Class<?> controllerClazz = controller.getClazz();
final Object namespace = getControllerNamespace(controllerClazz);
final Annotation classAnnotation = controllerClazz.getAnnotation(annotation);

if (classAnnotation != null) {
handleAction("*", namespace, controllerName, interceptor);
} else{
Arrays.stream(controllerClazz.getMethods())
.filter(method -> method.getAnnotation(annotation) != null && Modifier.isPublic(method.getModifiers()))
.forEach(methodAction -> handleAction(methodAction.getName(), namespace, controllerName, interceptor));

Arrays.stream(controllerClazz.getDeclaredFields())
.filter( field -> field.getAnnotation(annotation) != null )
.forEach( fieldAction -> handleAction(fieldAction.getName(), namespace, controllerName, interceptor));
}
}
}

private static void handleAction(String actionName, Object namespace, String controllerName, Interceptor interceptor) {
if(log.isDebugEnabled()) log.debug("Excluding namespace: {}, controller: {}, action: {} from interceptor: {}", namespace, controllerName, actionName, interceptor.getClass().getName());
LinkedHashMap<String,Object> args = new LinkedHashMap<>();
args.put("namespace", namespace);
args.put("controller", controllerName);
args.put("action", actionName);
interceptor.getMatchers().forEach(matcher -> matcher.except(args));
}

private static Object getControllerNamespace(Class<?> controllerClazz) {
return Arrays.stream(controllerClazz.getDeclaredFields())
.filter(field -> Objects.equals(GrailsControllerClass.NAMESPACE_PROPERTY, field.getName()) && Modifier.isStatic(field.getModifiers()))
.findFirst()
.map(field -> { try { return field.get(null); } catch (IllegalAccessException e) { return null; } })
.orElse(null);
}
}
12 changes: 12 additions & 0 deletions src/main/java/grails/plugin/inertia/annotation/SkipInertia.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package grails.plugin.inertia.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD})
public @interface SkipInertia {
String value() default "";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package grails.plugin.inertia.annotation

import grails.artefact.Interceptor
import grails.testing.web.interceptor.InterceptorUnitTest
import grails.web.Controller
import spock.lang.Specification

import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target

class AnnotationExcluderSpec extends Specification implements InterceptorUnitTest<AllMatchingInterceptor> {

def 'it excludes controller classes with annotation'() {
given:
grailsApplication.addArtefact('Controller', SkipClassController)

when:
withRequest(controller: 'skipClass', action: 'action')

then:
interceptor.doesMatch()

when:
AnnotationExcluder.excludeAnnotations(interceptor, grailsApplication, SkipAnnotation)

then:
!interceptor.doesMatch()
}

def 'it excludes controller method actions with annotation'() {
given:
grailsApplication.addArtefact('Controller', SkipMethodActionController)

when:
withRequest(controller: 'skipMethodAction', action: 'action')

then:
interceptor.doesMatch()

when:
AnnotationExcluder.excludeAnnotations(interceptor, grailsApplication, SkipAnnotation)

then:
!interceptor.doesMatch()
}

def 'it excludes controller closure actions with annotation'() {
given:
grailsApplication.addArtefact('Controller', SkipClosureActionController)

when:
withRequest(controller: 'skipClosureAction', action: 'action')

then:
interceptor.doesMatch()

when:
AnnotationExcluder.excludeAnnotations(interceptor, grailsApplication, SkipAnnotation)

then:
!interceptor.doesMatch()
}
}

@Controller
@SkipAnnotation
class SkipClassController {

@SuppressWarnings('unused')
def action() {}
}

@Controller
class SkipMethodActionController {

@SkipAnnotation
@SuppressWarnings('unused')
def action() {}
}

@Controller
class SkipClosureActionController {
@SkipAnnotation
@SuppressWarnings('unused')
def action = {}
}

class AllMatchingInterceptor implements Interceptor {
AllMatchingInterceptor() { matchAll() }
}

@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.METHOD, ElementType.TYPE, ElementType.FIELD])
@interface SkipAnnotation {
String value() default "";
}

0 comments on commit ec9e3c3

Please sign in to comment.