/*
* Copyright (C) 2014 Civilian Framework.
*
* Licensed under the Civilian License (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.civilian-framework.org/license.txt
*
* 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.civilian.internal;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.civilian.Application;
import org.civilian.Request;
import org.civilian.Resource;
import org.civilian.Response;
import org.civilian.content.ContentType;
import org.civilian.content.ContentTypeList;
import org.civilian.internal.intercept.ReqReaderInterceptorChain;
import org.civilian.internal.intercept.ReqStreamInterceptorChain;
import org.civilian.request.AsyncContext;
import org.civilian.request.RequestReaderInterceptor;
import org.civilian.request.RequestStreamInterceptor;
import org.civilian.resource.ExtensionMapping;
import org.civilian.resource.Path;
import org.civilian.resource.PathParam;
import org.civilian.text.LocaleService;
import org.civilian.util.Check;
/**
* AbstractRequest is a partial request implementation
* with useful defaults.
*/
public abstract class AbstractRequest implements Request
{
/**
* Creates a new AbstractRequest.
* @param application the associated application
* @param relativePath the relative path of the request with respect to the application path.
*/
public AbstractRequest(Application application, String relativePath)
{
application_ = Check.notNull(application, "application");
relativePath_ = new Path(relativePath);
}
protected AbstractRequest(AbstractRequest other)
{
application_ = other.getApplication();
relativePath_ = other.getRelativePath();
acceptedContentTypes_ = other.acceptedContentTypes_;
resource_ = other.resource_;
localeService_ = other.localeService_;
if (other.pathParams_ != null)
pathParams_ = new HashMap<>(other.pathParams_);
if (other.extension_ != null)
extension_ = new Extension(other.extension_);
}
//-----------------------------
// general accessors
//-----------------------------
@Override public Application getApplication()
{
return application_;
}
@Override public Response getResponse()
{
return response_;
}
@Override public void setResponse(Response response)
{
Check.notNull(response, "response");
if (response.getRequest() != this)
throw new IllegalArgumentException("not my response");
response_ = response;
}
//-----------------------------
// path related methods
//-----------------------------
protected void setRelativePath(String relativePath)
{
relativePath_ = new Path(relativePath);
path_ = null; // create lazy
if (extension_ != null)
extension_.matrixParams = null;
}
@Override public Path getPath()
{
if (path_ == null)
path_ = application_.getPath().add(relativePath_);
return path_;
}
@Override public Path getRelativePath()
{
return relativePath_;
}
//----------------------------
// resource
//----------------------------
@Override public Resource getResource()
{
return resource_;
}
@Override public void setResource(Resource resource)
{
resource_ = resource;
}
//----------------------------
// path params
//----------------------------
@Override public <T> void setPathParam(PathParam<T> pathParam, T value)
{
Check.notNull(pathParam, "pathParam");
Check.notNull(value, "value");
if (pathParams_ == null)
pathParams_ = new HashMap<>();
pathParams_.put(pathParam, value);
}
@Override public void setPathParams(Map<PathParam<?>,Object> pathParams)
{
pathParams_ = pathParams;
}
@SuppressWarnings("unchecked")
@Override public <T> T getPathParam(PathParam<T> pathParam)
{
return pathParams_ != null ? (T)pathParams_.get(pathParam) : null;
}
@SuppressWarnings("unchecked")
@Override public Iterator<PathParam<?>> getPathParams()
{
return pathParams_ != null ? pathParams_.keySet().iterator() : Collections.EMPTY_LIST.iterator();
}
protected void clearPathParams()
{
pathParams_ = null;
}
//----------------------------
// matrix params
//----------------------------
@Override public String getMatrixParam(String name)
{
return getMatrixParams().get(name);
}
@Override public String[] getMatrixParams(String name)
{
return getMatrixParams().getAll(name);
}
@Override public Iterator<String> getMatrixParamNames()
{
return getMatrixParams().iterator();
}
private ParamList getMatrixParams()
{
if (readExt().matrixParams == null)
parseMatrixParams();
return readExt().matrixParams;
}
private void parseMatrixParams()
{
String uri = getOriginalPath();
int slash = uri.lastIndexOf('/');
int colon = uri.indexOf(';', slash + 1);
if (colon < 0)
writeExt().matrixParams = ParamList.EMPTY;
else
{
ParamList matrixParams = writeExt().matrixParams = new ParamList();
int start = colon + 1;
do
{
int end = uri.indexOf(';', start);
if (end < 0)
end = uri.length();
int eq = uri.indexOf('=', start);
if ((eq < 0) || (eq > end))
matrixParams.add(uri.substring(start, end), "");
else if (eq > start)
matrixParams.add(uri.substring(start, eq), uri.substring(eq + 1, end));
start = end + 1;
}
while(start < uri.length());
}
}
//----------------------------
// preferences
//----------------------------
@Override public ContentTypeList getAcceptedContentTypes()
{
if (acceptedContentTypes_ == null)
initAcceptedContentTypes();
return acceptedContentTypes_;
}
// lazy construct the list of accepted content types
// we sort by quality, higher quality coming first, to speed up content negotiation
// if there are no accept headers, we want a list containing */*
private void initAcceptedContentTypes()
{
acceptedContentTypes_ = ContentTypeList.parse(ContentType.Compare.BY_QUALITY, getHeaders().getAll("Accept"));
if (acceptedContentTypes_.size() == 0)
{
ContentType contentType = getExtensionMapping().extractContentType(getRelativePath().getExtension());
acceptedContentTypes_ = contentType != null ?
new ContentTypeList(contentType) :
ContentTypeList.ANY;
}
}
protected ExtensionMapping getExtensionMapping()
{
return getApplication().getResourceConfig().getExtensionMapping();
}
//------------------------------
// locale
//------------------------------
/**
* Returns the locale data associated with the request.
* The locale data can be set explicitly by {@link #setLocaleService(LocaleService)}.
* If not explicitly set it is derived from the requested preferred locale ({@link #getAcceptedLocale()}).
* If the preferred locale is not contained in the list of supported locales (see {@link Application#getLocaleServices()})
* then the default application locale will be used.
*/
@Override public LocaleService getLocaleService()
{
// lazy init of LocaleService
// if during processing the locale-item is initialized by a call
// to #setLocaleItem (e.g. from a user profile), then we avoid to call getPreferences().getLocale())
if (localeService_ == null)
localeService_ = getApplication().getLocaleServices().getService(getAcceptedLocale());
return localeService_;
}
/**
* Sets the localeService associated with the request.
* This overrides the default locale data, as defined by the requested preferred language.
*/
@Override public void setLocaleService(LocaleService localeService)
{
localeService_ = Check.notNull(localeService, "localeService");
}
//------------------------------
// async
//------------------------------
/**
* Returns the AsyncContext that was created by the most recent call to {@link #startAsync()}
* @return the AsnycContext
* @throws IllegalStateException if startAsync() has not been called.
*/
@Override public AsyncContext getAsyncContext()
{
AsyncContext content = readExt().asyncContext;
if (content == null)
throw new IllegalStateException("async not started");
return content;
}
/**
* Returns if this request has been put into asynchronous mode by a call to {@link #startAsync()}.
*/
@Override public boolean isAsyncStarted()
{
return readExt().asyncContext != null;
}
/**
* Returns if this request supports asynchronous processing.
*/
@Override public abstract boolean isAsyncSupported();
/**
* Puts this request into asynchronous mode and initializes its AsyncContext.
* @throws IllegalStateException if this request does not support asynchronous operations or if called again
* in a state where the AsyncContext intervenes, or when the response has been closed.
*/
@Override public AsyncContext startAsync()
{
try
{
return writeExt().asyncContext = createAsyncContext();
}
catch(Exception e)
{
String message = "can't start async mode";
if (!isAsyncSupported())
message += ", not supported or enabled";
throw new IllegalArgumentException(message, e);
}
}
protected abstract AsyncContext createAsyncContext() throws Exception;
protected void resetAsyncContext()
{
if (extension_ != null)
extension_.asyncContext = null;
}
//------------------------------
// content
//------------------------------
/**
* Initializes the encoding to the application encoding
* if the encoding is not set and content-type is
* application/x-www-form-urlencoded.
* Derived implementation should call this method once
* they are initialized and allow access to their
* getContentType() implementation.
* Since the request implementation (e.g. HttpServletRequest)
* will read the from-urlencoded content to parse parameters)
* this initialization should be done before a parameter is accessed.
*/
protected void initEncoding()
{
if ((getContentEncoding() == null) &&
ContentType.APPLICATION_X_WWW_FORM_URLENCODED.equals(getContentType()))
{
setDefaultContentEncoding();
}
}
private void setDefaultContentEncoding()
{
String encoding = getApplication().getEncoding();
try
{
setContentEncoding(encoding);
}
catch (UnsupportedEncodingException e)
{
throw new IllegalStateException("application encoding '" + encoding + "' not supported", e);
}
}
@Override public ContentAccess getContentAccess()
{
if (contentInput_ == null)
return ContentAccess.NONE;
else if (contentInput_ instanceof Reader)
return ContentAccess.READER;
else
return ContentAccess.INPUTSTREAM;
}
@Override public ContentType getContentType()
{
ContentType contentType = readExt().contentType;
return contentType != null ? contentType : getContentTypeImpl();
}
protected abstract ContentType getContentTypeImpl();
@Override public void setContentType(ContentType contentType)
{
writeExt().contentType = contentType;
initEncoding();
}
@Override public InputStream getContentStream() throws IOException
{
if (!(contentInput_ instanceof InputStream))
initContentStream();
return (InputStream)contentInput_;
}
private void initContentStream() throws IOException
{
checkNoContentReader();
InputStream in = getContentStreamImpl();
// always save in contentInput_ before we try to apply the interceptors
// if an interceptor fails, we still want to have an assigned imputstream
contentInput_ = in;
RequestStreamInterceptor interceptor = readExt().streamInterceptor;
if (interceptor != null)
contentInput_ = ReqStreamInterceptorChain.intercept(this, in, interceptor);
}
protected abstract InputStream getContentStreamImpl() throws IOException;
/**
* Returns a Reader for the request content, using the character
* encoding of the content.
*/
@Override public Reader getContentReader() throws IOException
{
if (!(contentInput_ instanceof Reader))
initContentReader();
return (Reader)contentInput_;
}
private void initContentReader() throws IOException
{
checkNoContentStream();
if (getContentEncoding() == null)
setDefaultContentEncoding();
Reader reader;
Extension ext = readExt();
if (ext.streamInterceptor != null)
{
initContentStream();
reader = getContentReaderImpl((InputStream)contentInput_);
}
else
reader = getContentReaderImpl();
// always save in contentInput_ before we try to apply the interceptors
// if an interceptor fails, we still want to have an assigned reader
contentInput_ = reader;
if (ext.readerInterceptor != null)
contentInput_ = ReqReaderInterceptorChain.intercept(this, reader, ext.readerInterceptor);
}
protected abstract Reader getContentReaderImpl() throws IOException;
protected Reader getContentReaderImpl(InputStream out) throws IOException
{
return new InputStreamReader(out, getContentEncoding());
}
private void checkNoContentStream()
{
if (contentInput_ instanceof InputStream)
throw new IllegalStateException("Request.getContentStream() has already been called");
}
private void checkNoContentReader()
{
if (contentInput_ instanceof Reader)
throw new IllegalStateException("Request.getContentReader() has already been called");
}
protected void resetContentInput()
{
contentInput_ = null;
}
@Override public void addInterceptor(RequestStreamInterceptor interceptor)
{
checkAddInterceptor(interceptor);
Extension ext = writeExt();
ext.streamInterceptor = new ReqStreamInterceptorChain(ext.streamInterceptor, interceptor);
}
@Override public void addInterceptor(RequestReaderInterceptor interceptor)
{
checkAddInterceptor(interceptor);
Extension ext = writeExt();
ext.readerInterceptor = new ReqReaderInterceptorChain(ext.readerInterceptor, interceptor);
}
protected void clearInterceptors()
{
Extension ext = readExt();
if (ext.streamInterceptor != null)
ext.streamInterceptor = null;
if (ext.readerInterceptor != null)
ext.readerInterceptor = null;
}
private void checkAddInterceptor(Object interceptor)
{
Check.notNull(interceptor, "interceptor");
// getContentStream() or getContentWriter() may not be called yet
if (contentInput_ != null)
{
checkNoContentStream(); // fails or
checkNoContentReader(); // fails
}
}
//------------------------------
// ext
//------------------------------
/**
* Bundles properties which are null most of the times
*/
private static class Extension
{
public Extension()
{
}
public Extension(Extension other)
{
asyncContext = other.asyncContext;
matrixParams = other.matrixParams;
contentType = other.contentType;
streamInterceptor = other.streamInterceptor;
readerInterceptor = other.readerInterceptor;
}
public AsyncContext asyncContext;
public ParamList matrixParams;
public ContentType contentType;
public RequestStreamInterceptor streamInterceptor;
public RequestReaderInterceptor readerInterceptor;
}
private Extension readExt()
{
return extension_ != null ? extension_ : READ_EXTENSION;
}
private Extension writeExt()
{
if (extension_ == null)
extension_ = new Extension();
return extension_;
}
@Override public String toString()
{
return "request:" + getPath();
}
private Response response_;
private Path path_;
private Path relativePath_;
private Map<PathParam<?>, Object> pathParams_;
private LocaleService localeService_;
private Application application_;
private Object contentInput_;
private Resource resource_;
private Extension extension_;
protected ContentTypeList acceptedContentTypes_;
private static final Extension READ_EXTENSION = new Extension();
}