001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 *     http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014package org.gbif.ws.server.aspect;
015
016import org.gbif.api.annotation.NullToNotFound;
017import org.gbif.ws.NotFoundException;
018
019import java.net.URI;
020import java.util.Optional;
021
022import org.aspectj.lang.JoinPoint;
023import org.aspectj.lang.annotation.AfterReturning;
024import org.aspectj.lang.annotation.Aspect;
025import org.aspectj.lang.reflect.MethodSignature;
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028import org.springframework.stereotype.Component;
029import org.springframework.web.bind.annotation.GetMapping;
030import org.springframework.web.bind.annotation.RequestMapping;
031import org.springframework.web.util.UriComponentsBuilder;
032
033/**
034 * This aspect throws a {@link NotFoundException} for every {@code null} return value of a method.
035 */
036@Component
037@Aspect
038public class NullToNotFoundAspect {
039
040  private static final Logger LOG = LoggerFactory.getLogger(NullToNotFoundAspect.class);
041
042  @AfterReturning(
043      pointcut = "@annotation(org.gbif.api.annotation.NullToNotFound)",
044      returning = "retVal")
045  public void afterReturningAdvice(JoinPoint jp, Object retVal) {
046    if (retVal == null) {
047      NullToNotFound nullToNotFound = getAnnotation(jp);
048
049      // replace pat variables in URI with values
050      URI uri = getTargetUrl(jp, nullToNotFound);
051
052      throw new NotFoundException("Entity not found", uri);
053    }
054  }
055
056  /**
057   * Gets the NullToNotFound annotation.
058   */
059  private static NullToNotFound getAnnotation(JoinPoint jp) {
060    return ((MethodSignature) jp.getSignature()).getMethod().getAnnotation(NullToNotFound.class);
061  }
062
063  /**
064   * Builds the URL invoked by the request.
065   */
066  private static URI getTargetUrl(JoinPoint jp, NullToNotFound nullToNotFound) {
067    if (nullToNotFound.useUrlMapping()) {
068      return UriComponentsBuilder.newInstance()
069          .path(getResourceUrl(jp))
070          .path(getMethodResourceUrl(jp))
071          .build(jp.getArgs());
072    } else {
073      return UriComponentsBuilder.newInstance().path(nullToNotFound.value()).build(jp.getArgs());
074    }
075  }
076
077  /**
078   * Ensures the url is surrounded by '/'.
079   */
080  private static String addSurroundingSlashes(String url) {
081    String resultUrl = url.endsWith("/") ? url : url + '/';
082    return resultUrl.startsWith("/") ? resultUrl : '/' + resultUrl;
083  }
084
085  /**
086   * Gets the Resource URL from the RequestMapping annotation if it exists.
087   */
088  private static String getResourceUrl(JoinPoint jp) {
089    return Optional.ofNullable(jp.getTarget().getClass().getAnnotation(RequestMapping.class))
090        .map(rm -> rm.value()[0])
091        .map(NullToNotFoundAspect::addSurroundingSlashes)
092        .orElse("");
093  }
094
095  /**
096   * Gets the value of the GetMapping annotation.
097   */
098  private static String getMethodResourceUrl(JoinPoint jp) {
099    return Optional.ofNullable(
100            ((MethodSignature) jp.getSignature()).getMethod().getAnnotation(GetMapping.class))
101        .map(gm -> gm.value()[0])
102        .orElse("");
103  }
104}