/*
* 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;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;
import org.civilian.application.AppConfig;
import org.civilian.controller.ControllerService;
import org.civilian.controller.ControllerSignature;
import org.civilian.controller.ControllerType;
import org.civilian.internal.resource.ControllerTypeProvider;
import org.civilian.provider.ResponseProvider;
import org.civilian.resource.Path;
import org.civilian.resource.PathParam;
import org.civilian.resource.PathScanner;
import org.civilian.resource.Route;
import org.civilian.resource.Url;
import org.civilian.util.ArrayUtil;
import org.civilian.util.Check;
import org.civilian.util.IoUtil;
/**
* Resource represents a resource of a web application, addressable by an URL.
* A Resource can be associated with a {@link Controller}.<br>
* When the application receives a request for a resource,
* an instance of the associated Controller is created and invoked to generate
* the response.<br>
* If no resource directly matches the request,
* the controller of the closest matching parent resource will be used to process the request,
* using only the fallback actions of the controller.<br>
* There are several options how to construct the resource tree of an application:
* First the resource tree can be constructed at development or compile time by scanning
* the controller classes of an application. Second are more unusually the application
* can decide to build its resource tree by hand and map controller classes to it.
* It would also be possible to enhance the resource tree at runtime, and remap resources
* to different controllers.
*/
public class Resource implements Iterable<Resource>
{
/**
* Creates a new root resource.
*/
public Resource()
{
parent_ = null;
route_ = Route.root();
tree_ = new Tree(this);
segment_ = "";
pathParam_ = null;
}
/**
* Creates a new resource which extends the parent resource by a constant path segment.
* The route of the resource is the parent route + the segment.
* @param parent the parent resource
* @param segment the segment.
*/
public Resource(Resource parent, String segment)
{
this(parent, Check.notNull(segment, "segment"), null);
}
/**
* Creates a new resource which extends the parent resource by a path param.
* The route of the resource is the parent route + the pathParam
* @param parent the parent resource
* @param pathParam the path param
*/
public Resource(Resource parent, PathParam<?> pathParam)
{
this(parent, null, Check.notNull(pathParam, "pathParam"));
}
/**
* Creates a new resource.
* Either the segment or the path param has to be provided
* @param parent the parent resource
* @param segment the segment
* @param pathParam the path param
*/
public Resource(Resource parent, String segment, PathParam<?> pathParam)
{
if ((segment == null) == (pathParam == null))
throw new IllegalArgumentException("a segment or path param must be provided");
parent_ = Check.notNull(parent, "parent");
tree_ = parent.tree_;
if (segment != null)
{
segment_ = segment;
pathParam_ = null;
route_ = parent.route_.add(segment);
}
else
{
segment_ = null;
pathParam_ = pathParam;
route_ = parent.route_.add(pathParam);
Resource p = parent;
while(p != null)
{
if (p.pathParam_ == pathParam)
throw new IllegalArgumentException("the path parameter '" + pathParam + "' was already used in parent resource " + p);
p = p.parent_;
}
}
parent.addChild(this);
}
private synchronized void initTypeProvider(boolean recursive)
{
typeProvider_ = ControllerTypeProvider.create(this);
if (recursive)
{
for (Resource child : children_)
child.initTypeProvider(recursive);
}
}
private synchronized void addChild(Resource resource)
{
Resource[] children = ArrayUtil.addLast(children_, resource);
Arrays.sort(children, COMPARATOR);
children_ = children;
}
/**
* Returns the resource tree which exposes properties
* shared by all resources belonging to the tree.
*/
public Tree getTree()
{
return tree_;
}
/**
* Returns if this resource is the root resource.
*/
public boolean isRoot()
{
return parent_ == null;
}
/**
* Returns the root resource.
*/
public Resource getRoot()
{
Resource resource = this;
while(resource.parent_ != null)
resource = resource.parent_;
return resource;
}
/**
* Returns the parent resource or null if this is the root resource
*/
public Resource getParent()
{
return parent_;
}
/**
* Returns the segment by which this resource extends
* the parent resource. Returns null if the resource extends
* by a path-param. Returns "" if the resource is the root.
*/
public String getSegment()
{
return segment_;
}
/**
* Returns the PathParam by which this resource extends the parent resource
* Returns null, if the resource is the root or extends by a segment.
*/
public PathParam<?> getPathParam()
{
return pathParam_;
}
/**
* Returns the Route from the application root to this resource.
*/
public Route getRoute()
{
return route_;
}
/**
* Returns the controller signature. It describes the controller class
* which handles requests to this resource. See {@link ControllerSignature}
* for more info.
* @return the signature. If the resource is not a associated with
* a controller, null is returned.
*
*/
public String getControllerSignature()
{
return ctrlSignature_;
}
/**
* Sets the controller signature of the resource.
*/
public void setControllerSignature(String signature)
{
tree_.mapResource(this, ctrlSignature_, signature);
ctrlSignature_ = signature;
initTypeProvider(false);
}
/**
* Sets the ControllerInfo of the resource. The ControllerInfo
* describes the controller associated with the resource.
*/
public void setControllerSignature(String className, String methodPath)
{
setControllerSignature(ControllerSignature.build(className, methodPath));
}
/**
* Returns the ContollerType of the Controller associated with the resource.
* If the Resource does not have a Controller, null is returned.
* @throws IllegalStateException thrown if the resource has a controller,
* but the resource tree to which this Resource belongs is
* not connected to a ControllerService
*/
public ControllerType getControllerType() throws IllegalStateException
{
return typeProvider_.getControllerType(this);
}
/**
* Returns the number of resources in the subtree starting with this Resource.
*/
public int size()
{
int count = 1;
for (int i=getChildCount() - 1; i>=0; i--)
count += getChild(i).size();
return count;
}
/**
* Returns the number of child resources.
*/
public int getChildCount()
{
return children_.length;
}
/**
* Returns the i-th child resource.
*/
public Resource getChild(int i)
{
return children_[i];
}
/**
* Finds the descendant resource of this resource which matches
* the path.
* @param path a path string
* @return MatchResult stores the result of the match operation.
*/
public Match match(String path)
{
Resource resource = this;
boolean completeMatch = true;
PathScanner scanner = new PathScanner(path);
Map<PathParam<?>,Object> pathParams = new LinkedHashMap<>();
while (scanner.hasMore())
{
Resource child = resource.matchChild(scanner, pathParams);
if (child == null)
{
completeMatch = false;
break;
}
resource = child;
}
return new Match(resource, completeMatch, pathParams);
}
/**
* Returns the first child which matches segment given by the PathScanner.
* @return If not null, a matching child has been found. The PathScanner position
* has been moved past the matched segments. If path params have been recognized
* they were added to param map. Else null is returned.
*/
public Resource matchChild(PathScanner scanner, Map<PathParam<?>, Object> pathParams)
{
Resource[] children = children_;
for (Resource child : children)
{
if (child.matches(scanner, pathParams))
return child;
}
return null;
}
/**
* Returns if the resource matches segments given by the path scanner.
* @return If true, the resource matched one or more segments. The PathScanner position
* has been moved past those segments. If path params have been recognized
* they were added to param map.
*/
public boolean matches(PathScanner scanner, Map<PathParam<?>, Object> pathParams)
{
if (segment_ != null)
{
if (scanner.matchSegment(segment_))
{
scanner.next();
return true;
}
}
else
{
Object paramValue = pathParam_.parse(scanner);
if (paramValue != null)
{
pathParams.put(pathParam_, paramValue);
return true;
}
}
return false;
}
/**
* Recursively go trough the resource tree and instantiate the controller classes.
* This allows for a test during application startup (in production mode)
* if the resource tree has valid controller classes.
*/
public void touchControllerClasses() throws ClassNotFoundException
{
if (ctrlSignature_ != null)
Class.forName(ControllerSignature.getClassName(ctrlSignature_));
for (int i=0; i<getChildCount(); i++)
getChild(i).touchControllerClasses();
}
/**
* Prints the resource tree starting with this resource.
*/
public void print(PrintStream out)
{
print(new PrintWriter(out, true));
}
/**
* Prints the resource tree starting with this resource.
*/
public void print(PrintWriter out)
{
String s = toString();
out.print(s);
for (int i=s.length(); i<30; i++)
out.print(" ");
if (ctrlSignature_ != null)
{
out.print(" c=");
out.print(ctrlSignature_);
out.print(",");
}
out.println();
for (int i=0; i<getChildCount(); i++)
getChild(i).print(out);
}
/**
* Returns a depth-first iterator for the resource tree
* starting with this resource.
*/
@Override public Iterator<Resource> iterator()
{
return new It(this);
}
/**
* A depth-first iterator of the resource tree.
*/
private static class It implements Iterator<Resource>
{
public It(Resource root)
{
next_ = root;
}
@Override public boolean hasNext()
{
return next_ != null;
}
/**
* Advances to the next resource.
* @return returns false if the are no more resources to enumerate
*/
@Override public Resource next()
{
if (next_ == null)
throw new NoSuchElementException();
Resource result = next_;
int nextChild = 0;
while(next_ != null)
{
if (nextChild < next_.getChildCount())
{
next_ = next_.getChild(nextChild);
childIndexStack_.push(Integer.valueOf(++nextChild));
break;
}
if (childIndexStack_.isEmpty())
{
next_ = null;
break;
}
nextChild = childIndexStack_.pop().intValue();
next_ = next_.getParent();
}
return result;
}
@Override public void remove()
{
throw new UnsupportedOperationException();
}
private Resource next_;
private Stack<Integer> childIndexStack_ = new Stack<>();
}
/**
* MatchResult stores the result of a {@link Resource#match(String)} operation.
*/
public static class Match
{
public Match(Resource resource, boolean completeMatch, Map<PathParam<?>,Object> pathParams)
{
this.resource = resource;
this.completeMatch = completeMatch;
this.pathParams = pathParams;
}
/**
* Specifies if the resource completely matches the request path (true),
* or is the best partial match (false)
*/
public final boolean completeMatch;
/**
* The matched resource.
*/
public final Resource resource;
/**
* The path parameters collected during the match operation.
*/
public final Map<PathParam<?>,Object> pathParams;
}
@SuppressWarnings("serial")
private static class ResComparator implements Comparator<Resource>, Serializable
{
@Override public int compare(Resource o1, Resource o2)
{
if (o1.segment_ != null)
return o2.segment_ == null ? -1 : o1.segment_.compareTo(o2.segment_);
else
return o2.pathParam_ == null ? 1 : o1.pathParam_.getName().compareTo(o2.pathParam_.getName());
}
}
/**
* Tree stores properties shared by all resources
* belonging to the same tree.
*/
public static class Tree
{
private Tree(Resource root)
{
root_ = root;
}
/**
* Sets the application path for all resoures belonging to
* this resource tree.
* The path is automatically preprended to a {@link Url}
* when the Url is constructed from a Resource.
* When the application is initialized it will automatically initialize
* the path on it's own resource tree.
* @see Application#getPath()
* @see #getAppPath()
* @param appPath the app path
*/
public void setAppPath(Path appPath)
{
appPath_ = Check.notNull(appPath, "appPath");
}
/**
* Returns the path of the application to which this resource belongs.
* The path is automatically preprended to a {@link Url}
* when the Url is constructed from a Resource.
* Returns the root path if the resource is not attached to an application yet.
*/
public Path getAppPath()
{
return appPath_;
}
/**
* Makes the controller service available for all resources belonging to
* this resource tree.
*/
public void setControllerService(ControllerService service)
{
if (controllerService_ != service)
{
controllerService_ = service;
root_.initTypeProvider(true);
}
}
/**
* Returns the ControllerService associated with the resource tree.
* @see #setControllerService(ControllerService)
*/
public ControllerService getControllerService()
{
return controllerService_;
}
/**
* Sets the default extension used for all resources belonging to
* this resource tree
* The default extension is automatically added to a {@link Url}
* when the Url is constructed from a Resource.<br>
* If a default extension is defined in the application config, it will
* be automatically set on the applications resource tree during application setup.
* @param extension the extension or null if no default extension should be used.
* @see AppConfig#setDefaultResExtension(String)
* @see Url#Url(ResponseProvider, Resource)
*/
public void setDefaultExtension(String extension)
{
// we use a static method to express that it affects the whole tree
defaultExtension_ = IoUtil.normExtension(extension);
}
/**
* Returns the default extension which should be appended to
* Urls built for resource of this resource tree.
* When a Url is built using {@link Url#Url(ResponseProvider, Resource)}.
* the extension is automatically appended to the Url.
* @return the extension (without a leading dot), or null if
* no extension should be appended to the url.
*/
public String getDefaultExtension()
{
return defaultExtension_;
}
/**
* Returns the resource to which the controller with the given
* signature is mapped.
*/
public Resource getResource(String controllerSignature)
{
return sig2resource_.get(controllerSignature);
}
private void mapResource(Resource resource, String oldSignature, String newSignature)
{
if (oldSignature != null)
sig2resource_.remove(oldSignature);
if (newSignature != null)
sig2resource_.put(newSignature, resource);
}
private Resource root_;
private Path appPath_ = Path.ROOT;
private String defaultExtension_;
private ControllerService controllerService_;
private ConcurrentHashMap<String,Resource> sig2resource_ = new ConcurrentHashMap<>();
}
/**
* Returns a route representation of this resource.
*/
@Override public String toString()
{
return route_.toString();
}
private final Tree tree_;
private final Resource parent_;
private final String segment_;
private final PathParam<?> pathParam_;
private final Route route_;
private String ctrlSignature_;
private ControllerTypeProvider typeProvider_ = ControllerTypeProvider.EMPTY;
private Resource[] children_ = EMPTY;
private static Resource[] EMPTY = new Resource[0];
private static ResComparator COMPARATOR = new ResComparator();
}