3

In my App the admin user can control the URL for items, so I want to lookup registered URL's in the database, and redirect to the relevant controller method.

I am trying to figure out what I should be returning for a few scenarios:

  1. URL is missing - I want to throw a 404 error.
  2. URL is redirected - I want to return status 301 with the redirect back to the response.
  3. URL is okay - I want to redirect the URL to the relevant controller.

Noteworthy is the controller method uses standard requestMapping like /products/{productId} already, and resolves fine.

In the code, it finds the URL, and I can work out if it's a product, page etc. But I'm not sure how to redirect to the Controller method, or if the URL is redirected or doesn't exist how to return the error codes 301 or 404 respectively...

Can anyone help?

@Component
public class SeoUrlHandlerMapping extends AbstractUrlHandlerMapping {

    private static Logger logger = LogManager.getLogger(SeoUrlHandlerMapping.class.getName());

    @Autowired
    private ProductSeoService productSeoService;
    /**
     * Looks up the handler for the url path.
     * @param urlPath the URL path
     * @param request the request.
     * @return
     * @throws Exception
     */
    @Override
    protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {

        logger.entry("looking up handler for path: " + urlPath);

        // this is just a test.
        SeoUrl productUrl = productSeoService.findByURL(urlPath);
        if (productUrl instanceof ProductSeoUrl)
        {
            ProductSeoUrl productSeoUrl = (ProductSeoUrl) productUrl;
            logger.debug("Handling request to product  " + productSeoUrl.getProduct());
            request.setAttribute("id", productSeoUrl.getProduct().getId());
            return getApplicationContext().getBeansOfType(ProductWebController.class);
        }
        return null;
    }
}

2 Answers 2

4

Ok, well no answers, but I'll post what I ended up coming up with. I'm not sure if it's the best solution, but it seems to work okay for me. There's probably a better way to set up the model map, or rewrite path parameters, but servlet request was working okay...

So this is the main MappingHandler:

/**
 * The SeoUrlHandlerMapping will map between SEO URL requests and controller method
 */
@Component
public class SeoUrlHandlerMapping extends RequestMappingHandlerMapping {

    private static Logger logger = LogManager.getLogger(SeoUrlHandlerMapping.class.getName());

    @Autowired
    private ProductSeoService productSeoService;

    private final Map<String, HandlerMethod> handlerMethods = new LinkedHashMap<String, HandlerMethod>();


    @Override
    protected void initHandlerMethods() {

       logger.debug("initialising the handler methods");
        String[] beanNames =
                getApplicationContext().getBeanNamesForType(Object.class);

        for (String beanName : beanNames) {
            Class clazz = getApplicationContext().getType(beanName);
            final Class<?> userType = ClassUtils.getUserClass(clazz);

            if (isHandler(clazz)){
                for (Method method: clazz.getMethods())
                {
                    SeoUrlMapper mapper = AnnotationUtils.getAnnotation(method, SeoUrlMapper.class);
                    if (mapper != null)
                    {
                        RequestMappingInfo mapping = getMappingForMethod(method, userType);
                        HandlerMethod handlerMethod = createHandlerMethod(beanName, method);
                        this.handlerMethods.put(mapper.seoType(), handlerMethod);
                    }
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     * Expects a handler to have a type-level @{@link org.springframework.stereotype.Controller} annotation.
     */
    @Override
    protected boolean isHandler(Class<?> beanType) {
        return ((AnnotationUtils.findAnnotation(beanType, Controller.class) != null) ||
                (AnnotationUtils.findAnnotation(beanType, RequestMapping.class) != null));
    }

    /**
     * The lookup handler method, maps the SEOMapper method to the request URL.
     * <p>If no mapping is found, or if the URL is disabled, it will simply drop throug
     * to the standard 404 handling.</p>
     * @param urlPath the path to match.
     * @param request the http servlet request.
     * @return The HandlerMethod if one was found.
     * @throws Exception
     */
    @Override
    protected HandlerMethod lookupHandlerMethod(String urlPath, HttpServletRequest request) throws Exception {

        logger.entry("looking up handler for path: " + urlPath);

        // this is just a test.
        SeoUrl productUrl = productSeoService.findByURL(urlPath);
        if (productUrl instanceof ProductSeoUrl) {
            ProductSeoUrl productSeoUrl = (ProductSeoUrl) productUrl;

            if (productSeoUrl.getStatus().equals(SeoUrlStatus.OK) || productSeoUrl.getStatus().equals(SeoUrlStatus.DRAFT))
            {
                request.setAttribute(SeoConstants.ID, productSeoUrl.getProduct().getId());
                request.setAttribute(SeoConstants.URL_STATUS, productSeoUrl.getStatus().toString());
                return this.handlerMethods.get("PRODUCT");
            }else if (productSeoUrl.getStatus().equals(SeoUrlStatus.REDIRECTED))
            {
                request.setAttribute(SeoConstants.REDIRECT_URL, productSeoUrl.getRedirectURL());
                return this.handlerMethods.get("REDIRECT");
            }

            // otherwise we let it return 404 by dropping through.
        }

        return null;
    }
}

Then I used a custom annotation on Controller methods to isolate the handler methods:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SeoUrlMapper {
        /**
         * Assigns a type to this mapping.
         * <p><b>This should match the SEOEntityType constants</b></p>.
         */
        String seoType();
}

And finally in my Controller methods I set the annotations to indicate the methods:

@SeoUrlMapper(seoType = "REDIRECT")
public RedirectView issueRedirect(ModelMap map, HttpServletRequest request)
{
    logger.entry();
    RedirectView view = new RedirectView();
    view.setStatusCode(HttpStatus.MOVED_PERMANENTLY);
    view.setUrl((String)request.getAttribute("REDIRECT_URL"));
    view.setExposeModelAttributes(false);
    logger.exit();
    return view;
}

@SeoUrlMapper(seoType = "PRODUCT")
public String viewProductInternal(ModelMap map, HttpServletRequest request)
{
    Long id = (Long) request.getAttribute(SeoConstants.ID);
    Product product = productService.findForDetailView(id);
    return commonViewProduct(product, map);
}   
Sign up to request clarification or add additional context in comments.

1 Comment

Very interesting way to implement it.
1

I'm not sure if you really need a HandlerMapping for this. Couldn't the Controller handle all these cases?

URL is missing - I want to throw a 404 error.

I think the best way to do this is to write your own Exception (e.g. MissingUrlException), throw this exception if applicable and write your own error handler, e.g.

@ExceptionHandler(MissingUrlException.class)
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public String handleMissingUrl(MissingUrlException ex) {
    // return a view or anything you like
}

URL is redirected - I want to return status 301 with the redirect back to the response.

You could do the same here but set a 301 (i.e. HttpStatus.MOVED_PERMANENTLY) and use one of the options described in A Guide To Spring Redirects.

URL is okay - I want to redirect the URL to the relevant controller.

Just do your business logic in the method and return a view or something like that.

1 Comment

Thanks - the HandlerMapping isn't for these cases particularly, it's used to lookup a URL request in the database and then forward the request to the right controller based on the URL type. If it's in the controller code already it's okay to use your method, it's just standard spring mvc approach, but for this case it's still at the custom handler level when the url exception should be thrown, hence the question.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.