/* * Copyright 2013 cruxframework.org. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package org.cruxframework.crux.core.server.rest.core.dispatch; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.cruxframework.crux.core.config.ConfigurationFactory; import org.cruxframework.crux.core.server.rest.core.EntityTag; import org.cruxframework.crux.core.server.rest.core.dispatch.ResourceMethod.MethodReturn; import org.cruxframework.crux.core.server.rest.spi.HttpRequest; import org.cruxframework.crux.core.server.rest.spi.HttpResponse; import org.cruxframework.crux.core.server.rest.spi.UriInfo; import org.cruxframework.crux.core.server.rest.state.ETagHandler; import org.cruxframework.crux.core.server.rest.state.ResourceStateConfig; import org.cruxframework.crux.core.server.rest.state.ResourceStateHandler; import org.cruxframework.crux.core.server.rest.state.ResourceStateHandler.ResourceState; import org.cruxframework.crux.core.server.rest.util.DateUtil; import org.cruxframework.crux.core.server.rest.util.HttpHeaderNames; import org.cruxframework.crux.core.server.rest.util.HttpResponseCodes; import org.cruxframework.crux.core.shared.rest.annotation.GET; import org.cruxframework.crux.core.shared.rest.annotation.HttpMethod; /** * @author Thiago da Rosa de Bustamante * */ public class StateHandler { private static final Log logger = LogFactory.getLog(StateHandler.class); private static final Lock eTagLock = new ReentrantLock(); private static ETagHandler eTagHandler = null; private final HttpRequest request; private final ResourceMethod resourceMethod; private String httpMethod; private String key; private HttpResponse response; public StateHandler(ResourceMethod resourceMethod, HttpRequest request, HttpResponse response) { this.resourceMethod = resourceMethod; this.request = request; this.response = response; this.httpMethod = request.getHttpMethod(); this.key = request.getUri().getRequestUri().toString(); } public MethodReturn handledByCache() throws Exception { MethodReturn ret = null; if (resourceMethod.cacheInfo != null && resourceMethod.cacheInfo.isCacheEnabled()) { ret = handleCacheableOperation(); } else { ret = handleUncacheableOperation(); } if (ret == null) { ret = resourceMethod.doInvoke(request, response); if (ret.getCheckedExceptionData() == null) { updateState(request.getUri(), ret); } } return ret; } public void updateState(UriInfo uriInfo, MethodReturn ret) { ResourceStateHandler resourceStateHandler = ResourceStateConfig.getResourceStateHandler(); if (ret.getCacheInfo() != null && (ret.getCacheInfo().isCacheEnabled() || ret.isEtagGenerationEnabled())) // only GET can declare cache { long dateModified; long expires = 0; String etag; ResourceState resourceState = resourceStateHandler.get(key); if (!ret.getCacheInfo().isCacheEnabled()) { expires = (GET.ONE_DAY*1000)+System.currentTimeMillis(); } if (resourceState != null && !resourceState.isExpired()) { etag = resourceState.getEtag(); dateModified = resourceState.getDateModified(); if (ret.getCacheInfo().isCacheEnabled()) { expires = ret.getCacheInfo().defineExpires(System.currentTimeMillis()); } } else { etag = getETagHandler().generateEtag(uriInfo, ret.getReturn()); dateModified = System.currentTimeMillis(); if (ret.getCacheInfo().isCacheEnabled()) { expires = ret.getCacheInfo().defineExpires(dateModified); } } resourceStateHandler.add(key, dateModified, expires, etag); ret.setDateModified(dateModified); EntityTag entityTag = (etag != null)?new EntityTag(etag):null; ret.setEtag(entityTag); } else { resourceStateHandler.remove(key); } } /** * Handle Cacheable GETS * @return */ private MethodReturn handleCacheableOperation() throws Exception { ResourceStateHandler resourceStateHandler = ResourceStateConfig.getResourceStateHandler(); ResourceState resourceState = resourceStateHandler.get(key); MethodReturn ret = null; if (resourceState == null) { return null; } else { if (resourceState.isExpired()) { ret = resourceMethod.doInvoke(request, response); if (ret.getCheckedExceptionData() == null) { updateState(request.getUri(), ret); resourceState = resourceStateHandler.get(key); } else { resourceStateHandler.remove(key); } } ConditionalResponse conditionalResponse = evaluatePreconditions(resourceState); if (conditionalResponse != null) { return new MethodReturn(resourceMethod.hasReturnType, null, null, resourceMethod.cacheInfo, conditionalResponse, resourceMethod.isEtagGenerationEnabled()); } } return ret; } /** * Handle PUT/POST/DELETE/uncacheable GETs * @return */ private MethodReturn handleUncacheableOperation() throws Exception { ResourceStateHandler resourceStateHandler = ResourceStateConfig.getResourceStateHandler(); ResourceState resourceState = resourceStateHandler.get(key); MethodReturn ret = null; if (resourceMethod.getHttpMethod().equals(HttpMethod.GET) && resourceState != null && resourceState.isExpired()) { ret = resourceMethod.doInvoke(request, response); if (ret.getCheckedExceptionData() == null) { updateState(request.getUri(), ret); resourceState = resourceStateHandler.get(key); } else { resourceStateHandler.remove(key); resourceState = null; } } ConditionalResponse conditionalResponse = evaluatePreconditions(resourceState); if (conditionalResponse == null) { return ret; } return new MethodReturn(resourceMethod.hasReturnType, null, null, resourceMethod.cacheInfo, conditionalResponse, resourceMethod.isEtagGenerationEnabled()); } private ConditionalResponse evaluatePreconditions(ResourceState resourceState) { ConditionalResponse etag = evaluateEtagPreConditions(resourceState); ConditionalResponse dateModified = evaluateDateModifiedPreConditions(resourceState); if (etag == null && dateModified == null) { return null; } else if (etag != null && dateModified == null) { return etag; } else if (etag == null && dateModified != null) { return dateModified; } // (etag != null && dateModified != null) etag.setLastModified(dateModified.getLastModified()); return etag; } private ConditionalResponse evaluateEtagPreConditions(ResourceState resourceState) { ConditionalResponse result = null; EntityTag eTag = (resourceState!= null && resourceState.getEtag() != null)?new EntityTag(resourceState.getEtag()):null; List<String> ifMatch = request.getHttpHeaders().getRequestHeader(HttpHeaderNames.IF_MATCH); if (ifMatch != null && ifMatch.size() > 0) { if (!ifMatch(convertEtag(ifMatch), eTag)) { result = new ConditionalResponse(eTag, 0, HttpResponseCodes.SC_PRECONDITION_FAILED); } } if (result == null) { List<String> ifNoneMatch = request.getHttpHeaders().getRequestHeader(HttpHeaderNames.IF_NONE_MATCH); if (ifNoneMatch != null && ifNoneMatch.size() > 0) { if (!ifNoneMatch(convertEtag(ifNoneMatch), eTag)) { if (httpMethod.equals("GET")) { result = new ConditionalResponse(eTag, 0, HttpResponseCodes.SC_NOT_MODIFIED); } else { result = new ConditionalResponse(eTag, 0, HttpResponseCodes.SC_PRECONDITION_FAILED); } } } } return result; } private ConditionalResponse evaluateDateModifiedPreConditions(ResourceState resourceState) { ConditionalResponse result = null; String ifModifiedSince = request.getHttpHeaders().getHeaderString(HttpHeaderNames.IF_MODIFIED_SINCE); long dateModified = (resourceState!=null?resourceState.getDateModified():0); if (ifModifiedSince != null) { if (!ifModifiedSince(ifModifiedSince, dateModified)) { result = new ConditionalResponse(null, dateModified, HttpResponseCodes.SC_NOT_MODIFIED); } } if (result == null) { String ifUnmodifiedSince = request.getHttpHeaders().getHeaderString(HttpHeaderNames.IF_UNMODIFIED_SINCE); if (ifUnmodifiedSince != null) { if (!ifUnmodifiedSince(ifUnmodifiedSince, dateModified)) { result = new ConditionalResponse(null, dateModified, HttpResponseCodes.SC_PRECONDITION_FAILED); } } } return result; } private boolean ifMatch(List<EntityTag> ifMatch, EntityTag eTag) { if (eTag != null) { for (EntityTag tag : ifMatch) { if (tag.equals(eTag) || tag.getValue().equals("*")) { return true; } } } return false; } private boolean ifNoneMatch(List<EntityTag> ifMatch, EntityTag eTag) { if (eTag != null) { if (ifMatch != null && ifMatch.size() > 0) { for (EntityTag tag : ifMatch) { if (tag.equals(eTag) || tag.getValue().equals("*")) { return false; } } } } return true; } private boolean ifModifiedSince(String strDate, long lastModified) { Date date = DateUtil.parseDate(strDate); if (date.getTime() >= lastModified) { return false; } return true; } private boolean ifUnmodifiedSince(String strDate, long lastModified) { Date date = DateUtil.parseDate(strDate); if (date.getTime() >= lastModified) { return true; } return false; } private List<EntityTag> convertEtag(List<String> tags) { ArrayList<EntityTag> result = new ArrayList<EntityTag>(); if (tags != null) { for (String tag : tags) { String[] split = tag.split(","); for (String etag : split) { result.add(EntityTag.valueOf(etag.trim())); } } } return result; } public static ETagHandler getETagHandler() { if (eTagHandler != null) return eTagHandler; try { eTagLock.lock(); if (eTagHandler != null) return eTagHandler; eTagHandler = (ETagHandler) Class.forName(ConfigurationFactory.getConfigurations().eTagHandler()).newInstance(); } catch (Exception e) { logger.error("Error initializing eTagHandler.", e); } finally { eTagLock.unlock(); } return eTagHandler; } }