/*
* 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.resource;
import java.util.ArrayList;
import org.civilian.Application;
import org.civilian.Controller;
import org.civilian.Resource;
import org.civilian.Resource.Tree;
import org.civilian.Response;
import org.civilian.controller.ControllerSignature;
import org.civilian.provider.PathParamProvider;
import org.civilian.provider.PathProvider;
import org.civilian.provider.ResponseProvider;
import org.civilian.resource.scan.ResourceScan;
import org.civilian.response.UriEncoder;
import org.civilian.type.Type;
import org.civilian.type.TypeLib;
import org.civilian.type.fn.TypeSerializer;
import org.civilian.util.Check;
/**
* Url allows to build URLs either to address resources within an {@link Application}
* or arbitrary URLs.<p>
* In the first case the target resource can be specified by a {@link Resource} object.
* The Url will automatically derive the (absolute) path to that resource.
* Please see the documentation for {@link ResourceScan} how to generate a class
* at development time which contains constants for all resources used in your application.<p>
* The path to the resource may contain segments mapped to path parameters. The Url class provides
* an API to conveniently set these path parameters. If the request was dispatched to
* a resource, the path parameters shared by the request are automatically initialized.
* <p>
* The Url class also allows to set query parameters. Locale dependent serialization of parameter objects
* to parameter strings is supported.
* <p>
* Path parameter values and parameter values are automatically percent encoded, using the
* {@link Response#getUriEncoder()} UriEncoder} of the response. The Url class also takes
* care to add the session id to URL if needed.
*/
public class Url implements PathParamProvider, ResponseProvider
{
/**
* Creates a Url consisting of the given string value.
* @param rp a Response or a ResponseProvider whose Response is used by the Url
* @param url either a relative or absolute path to address a resource on
* the same server or a fully qualified URL.
* In the later case the Url will not add a session id to
* the url string
* @see #addSessionId(boolean)
*/
public Url(ResponseProvider rp, String url)
{
Check.notNull(rp, "response provider");
Check.notNull(url, "url");
response_ = rp.getResponse();
prefix_ = url;
pathParams_ = EMPTY_PATH_PARAMS;
}
/**
* Creates a Url consisting of the path provided
* by the PathProvider.
* @param rp a Response or a ResponseProvider whose Response is used by the Url
* @param pathProvider provides a path. Classes like Context, Application
* Request, etc. are examples of PathProviders.
*/
public Url(ResponseProvider rp, PathProvider pathProvider)
{
this(rp, Check.notNull(pathProvider, "pathProvider").getPath());
}
/**
* Creates a Url consisting of the path.
* @param rp a Response or a ResponseProvider whose Response is used by the Url
* @param path a path.
*/
public Url(ResponseProvider rp, Path path)
{
Check.notNull(rp, "response provider");
Check.notNull(path, "path");
response_ = rp.getResponse();
pathParams_ = EMPTY_PATH_PARAMS;
additionalPath_ = path;
}
/**
* Creates a Url consisting of the path to the resource whose
* controller has the given class.<br>
* The path may contain path parameters for which you then must provide
* values. Path parameters shared by the current request are automatically initialized.
* If the application defines a default extension for resource urls,
* it is also appended to the Url.<br>
* Please note that a controller may be mapped to several resources. In this case
* it is undefined which resource is chosen to build the url.
* @param rp a Response or a ResponseProvider whose Response is used by the Url
* @param controllerClass the controller class.
* @see Tree#getDefaultExtension()
*/
public <C extends Controller> Url(ResponseProvider rp, Class<C> controllerClass)
{
Response response = Check.notNull(rp, "response provider").getResponse();
String sig = ControllerSignature.build(controllerClass.getName(), null);
Resource resource = response.getApplication().getRootResource().getTree().getResource(sig);
if (resource == null)
throw new IllegalArgumentException(controllerClass.getName() + " not mapped to a resource");
init(response, resource);
}
/**
* Creates a Url consisting of the path to the given resource.
* The path may contain path parameters for which you then must provide
* values. Path parameters shared by the current request are automatically initialized.
* If the application defines a default extension for resource urls,
* it is also appended to the Url.
* @param rp a Response or a ResponseProvider whose Response is used by the Url
* @param resource the resource. The resource must not belong to the application which
* received the request.
* @see Tree#getDefaultExtension()
*/
public Url(ResponseProvider rp, Resource resource)
{
init(Check.notNull(rp, "response provider").getResponse(),
Check.notNull(resource, "resource"));
}
private void init(Response response, Resource resource)
{
response_ = response;
resource_ = resource;
prefix_ = resource.getTree().getAppPath().toString();
int ppCount = resource_.getRoute().getPathParamCount();
if (ppCount > 0)
{
pathParams_ = new Object[ppCount];
copyPathParams(response_.getRequest());
}
else
pathParams_ = EMPTY_PATH_PARAMS;
}
private Route getRoute()
{
if (resource_ == null)
throw new IllegalArgumentException("URL not defined from a resource");
return resource_.getRoute();
}
/**
* Returns the resource if the Url was constructed from a resource or
* from a controller class. Else it returns null.
*/
public Resource getResource()
{
return resource_;
}
/**
* Returns the response which was passed to the Url constructor.
*/
@Override public Response getResponse()
{
return response_;
}
/**
* Explicitly sets the TypeSerializer used by the Url when it
* formats typed parameters to a parameter string.
* @see QueryParam#setValue(Type, Object)
* @return this
*/
public Url setSerializer(TypeSerializer serializer)
{
serializer_ = serializer;
return this;
}
/**
* Returns the TypeSerializer used by the Url when it formats
* typed parameters to a parameter string.
* By the default the Url uses the serializer of the response.
* @see QueryParam#setValue(Type, Object)
* @see Response#getLocaleSerializer()
*/
public TypeSerializer getSerializer()
{
if (serializer_ == null)
serializer_ = response_.getLocaleSerializer();
return serializer_;
}
/**
* Prepends a url snippet at the beginning of the url string.
* @return this.
*/
public Url prepend(String url)
{
Check.notNull(url, "url");
if (prefix_ == null)
prefix_ = url;
else if (url.endsWith("/") && prefix_.startsWith("/"))
prefix_ = url + prefix_.substring(1);
else
prefix_ = url + prefix_;
return this;
}
/**
* Appends a path snippet to the Url.
* @return this
*/
public Url append(String path)
{
if (additionalPath_ != null)
additionalPath_ = additionalPath_.add(path);
else
additionalPath_ = new Path(path);
return this;
}
//---------------------------------
// path parameters
//---------------------------------
/**
* Returns the number of path parameters within the Url.
* A positive count is only possible for Urls which have been
* constructed by passing a Resource object to the
* {@link Url#Url(ResponseProvider, Resource) constructor}.
*/
public int getPathParamCount()
{
return pathParams_.length;
}
/**
* Returns the i-th path param value.
*/
public Object getPathParam(int index)
{
return pathParams_[index];
}
/**
* Returns path param value belonging to the given
* PathParam.
*/
@SuppressWarnings("unchecked")
@Override public <T> T getPathParam(PathParam<T> pathParam)
{
int index = getRoute().indexOf(pathParam);
return index >= 0 ? (T)pathParams_[index] : null;
}
/**
* Returns the i-th PathParam object which defines the i-th
* path parameter.
*/
public PathParam<?> getPathParamDef(int index)
{
return getRoute().getPathParam(index);
}
/**
* Sets the value of the first path parameter.
* @return this
*/
public Url setPathParam(Object value)
{
setPathParam(0, value);
return this;
}
/**
* Sets the value of the i-th path parameter.
* @see #getPathParamCount()
* @return this
*/
public Url setPathParam(int index, Object value)
{
pathParams_[index] = value;
return this;
}
/**
* Sets the path parameters.
* @return this
*/
public Url setPathParams(Object... values)
{
for (int i=0; i<values.length; i++)
setPathParam(i, values[i]);
return this;
}
/**
* Copies the path parameters from the provider to this url.
* @return this
*/
public Url copyPathParams(PathParamProvider provider)
{
Check.notNull(provider, "provider");
getRoute().extractPathParams(provider, pathParams_);
return this;
}
/**
* Sets the path parameter who is defined by the PathParam.
* @return this
*/
public <T> Url setPathParam(PathParam<T> param, T value)
{
int index = getRoute().indexOf(param);
if (index < 0)
throw new IllegalArgumentException("url does not contain a value for path parameter " + param);
pathParams_[index] = value;
return this;
}
/**
* Clears all path parameters.
* @return this
*/
public Url clearPathParams()
{
for (int i=0; i<pathParams_.length; i++)
setPathParam(i, null);
return this;
}
//---------------------------------
// query parameters
//---------------------------------
/**
* Clears all parameters of the Url.
* @return this
*/
public Url clearQueryParams()
{
if (queryParams_ != null)
queryParams_.clear();
return this;
}
/**
* Returns the number of query parameters.
*/
public int getQueryParamCount()
{
return queryParams_ != null ? queryParams_.size() : 0;
}
/**
* Returns the i-th query parameter.
*/
public QueryParam getQueryParam(int i)
{
return queryParams_.get(i);
}
/**
* Returns the first query parameter with the given name.
* @param create if true and the url does not contain such a parameter
* a new parameter is added.
*/
public QueryParam getQueryParam(String name, boolean create)
{
for (int i=0; i<getQueryParamCount(); i++)
{
if (queryParams_.get(i).name_.equals(name))
return queryParams_.get(i);
}
return create ? addQueryParam(name) : null;
}
/**
* Removes all query parameters with the specified name.
* @return this
*/
public Url removeQueryParams(String name)
{
for (int i=getQueryParamCount()-1; i>=0; i--)
{
if (queryParams_.get(i).name_.equals(name))
queryParams_.remove(i);
}
return this;
}
/**
* Removes the query parameter from the url.
* @return this
*/
public Url removeQueryParam(QueryParam param)
{
if (queryParams_ != null)
queryParams_.remove(param);
return this;
}
/**
* Adds a new query parameter to the Url.
* Use the setters on the returned param object to set the parameter value.
*/
public QueryParam addQueryParam(String name)
{
QueryParam param = new QueryParam(name);
if (queryParams_ == null)
queryParams_ = new ArrayList<>();
queryParams_.add(param);
return param;
}
/**
* Adds a new query parameter to the Url.
* @return this
*/
public Url addEmptyQueryParam(String name)
{
addQueryParam(name);
return this;
}
/**
* Adds a query parameter with that name and value.
* @return this
*/
public Url addQueryParam(String name, String value)
{
addQueryParam(name).setValue(value);
return this;
}
/**
* Adds multiple query parameter with that name and values.
* @return this
*/
public Url addQueryParams(String name, String... values)
{
for (String value : values)
addQueryParam(name, value);
return this;
}
/**
* Adds a query parameter with that name and value.
* @return this
*/
public Url addQueryParam(String name, int value)
{
addQueryParam(name).setValue(value);
return this;
}
/**
* Adds a query parameter with that name and value.
* @return this
*/
public Url addQueryParam(String name, Integer value)
{
addQueryParam(name).setValue(value);
return this;
}
/**
* Adds a query parameter with that name and value.
* @return this
*/
public Url addQueryParam(String name, boolean value)
{
addQueryParam(name).setValue(value);
return this;
}
/**
* Adds a query parameter with that name and value.
* @return this
*/
public Url addQueryParam(String name, Boolean value)
{
addQueryParam(name).setValue(value);
return this;
}
/**
* Adds a query parameter with that name and value.
* @return this
*/
public <T> Url addQueryParam(String name, Type<T> type, T value)
{
addQueryParam(name).setValue(type, value);
return this;
}
/**
* QueryParam models a linked list of URL parameters.
* The {@link Url} class uses Param to store its parameters.
*/
public class QueryParam
{
public QueryParam(String name)
{
name_ = Check.notNull(name, "name");
}
/**
* Returns the param name.
*/
public String getName()
{
return name_;
}
/**
* Returns the param value.
*/
public String getValue()
{
return value_;
}
/**
* Set the value of the Parameter to a string.
*/
public void setValue(String value)
{
value_ = value;
}
/**
* Sets the int value of the Parameter.
*/
public void setValue(int value)
{
setValue(Integer.valueOf(value));
}
public void setValue(Integer value)
{
setValue(TypeLib.INTEGER, value);
}
/**
* Sets the boolean value of the Parameter.
*/
public void setValue(boolean value)
{
setValue(TypeLib.BOOLEAN, Boolean.valueOf(value));
}
/**
* Sets the boolean value of the Parameter.
*/
public void setValue(Boolean value)
{
setValue(TypeLib.BOOLEAN, value);
}
/**
* Sets the value of the Parameter.
* The Urls TypeSerializer is used to convert the value to a string.
* @see Url#getSerializer()
*/
public <T> void setValue(Type<T> type, T value)
{
value_ = getSerializer().format(type, value);
}
void append(UriEncoder encoder, StringBuilder s)
{
encoder.encode(name_, s);
s.append('=');
if (value_ != null)
encoder.encode(value_, s);
}
private String name_;
private String value_;
}
//---------------------------------
// session
//---------------------------------
/**
* Sets if the server adds a session id to the Url
* string, when {@link #toString()} is called.
* By default the session id is not added.
* @return this
*/
public Url addSessionId(boolean mode)
{
addSessionId_ = mode;
return this;
}
/**
* Returns if the server adds a session id to the Url
* string, when {@link #toString()} is called.
* By default the session id will not be added.
*/
public boolean addSessionId()
{
return addSessionId_;
}
//---------------------------------
// fragment
//---------------------------------
/**
* Specifies the fragment which should be added to the end of the Url.
* @return this
*/
public Url setFragment(String fragment)
{
fragment_ = fragment;
return this;
}
/**
* Returns the fragment which will be added to the end of the Url.
*/
public String getFragment()
{
return fragment_;
}
//---------------------------------
// serialize
//---------------------------------
public String toQueryString()
{
return toString(false, true);
}
public String toQuerylessString()
{
return toString(true, false);
}
/**
* Converts the Url to a string.
*/
@Override public String toString()
{
return toString(true, true);
}
/**
* Converts the Url to a string.
* @param mainPart include the main part (the part before query parameters and fragment)
* @param queryString include query string and fragment)
*/
public String toString(boolean mainPart, boolean queryString)
{
StringBuilder s = new StringBuilder();
if (mainPart)
{
if (prefix_ != null)
s.append(prefix_);
if (resource_ != null)
{
resource_.getRoute().build(pathParams_, response_.getUriEncoder(), s);
if (additionalPath_ == null)
{
String ext = resource_.getTree().getDefaultExtension();
if (ext != null)
{
s.append('.');
s.append(ext);
}
}
}
if (additionalPath_ != null)
additionalPath_.addTo(s);
}
if (queryString)
{
if (queryParams_ != null)
{
int n = queryParams_.size();
for (int i=0; i<n; i++)
{
s.append(i == 0 ? '?' : '&');
queryParams_.get(i).append(response_.getUriEncoder(), s);
}
}
if (fragment_ != null)
{
s.append('#');
s.append(fragment_);
}
}
String url = s.toString();
if (addSessionId_)
url = response_.addSessionId(url);
return url;
}
private boolean addSessionId_;
private Response response_;
private String fragment_;
private TypeSerializer serializer_;
private Resource resource_;
private Path additionalPath_;
private Object[] pathParams_;
private ArrayList<QueryParam> queryParams_;
private String prefix_;
private static final Object[] EMPTY_PATH_PARAMS = new Object[0];
}