/*
s * Copyright 2013 mpowers
*
* 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 com.trsst.server;
import java.io.IOException;
import java.io.StringReader;
import java.net.InetAddress;
import java.util.Collection;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.security.auth.Subject;
import org.apache.abdera.Abdera;
import org.apache.abdera.model.Feed;
import org.apache.abdera.model.Workspace;
import org.apache.abdera.parser.ParseException;
import org.apache.abdera.parser.Parser;
import org.apache.abdera.protocol.Request;
import org.apache.abdera.protocol.server.CollectionAdapter;
import org.apache.abdera.protocol.server.CollectionInfo;
import org.apache.abdera.protocol.server.Filter;
import org.apache.abdera.protocol.server.FilterChain;
import org.apache.abdera.protocol.server.ProviderHelper;
import org.apache.abdera.protocol.server.RequestContext;
import org.apache.abdera.protocol.server.RequestProcessor;
import org.apache.abdera.protocol.server.ResponseContext;
import org.apache.abdera.protocol.server.Target;
import org.apache.abdera.protocol.server.TargetType;
import org.apache.abdera.protocol.server.Transactional;
import org.apache.abdera.protocol.server.WorkspaceInfo;
import org.apache.abdera.protocol.server.WorkspaceManager;
import org.apache.abdera.protocol.server.context.RequestContextWrapper;
import org.apache.abdera.protocol.server.context.ResponseContextException;
import org.apache.abdera.protocol.server.filters.OpenSearchFilter;
import org.apache.abdera.protocol.server.impl.AbstractWorkspaceProvider;
import org.apache.abdera.protocol.server.impl.RegexTargetResolver;
import org.apache.abdera.protocol.server.impl.SimpleCollectionInfo;
import org.apache.abdera.protocol.server.impl.TemplateTargetBuilder;
import com.trsst.Common;
/**
* Abdera-specific configuration of mapping targets and servlet filters.
*
* @author mpowers
*/
public class AbderaProvider extends AbstractWorkspaceProvider implements
WorkspaceInfo {
Hashtable<String, TrsstAdapter> idsToAdapters = new Hashtable<String, TrsstAdapter>();
String hostname;
public AbderaProvider() {
try {
hostname = InetAddress.getLocalHost().getHostName();
getStorage(); // force storage to load if necessary
} catch (Throwable t) {
log.info("Could not obtain hostname: defaulting to 'localhost'");
hostname = "localhost";
}
}
@Override
public void init(Abdera abdera, Map<String, String> properties) {
// can receive servlet init params here
super.init(abdera, properties);
// map paths to handlers
RegexTargetResolver resolver = new OrderedRegexTargetResolver();
resolver.setPattern("/service", TargetType.TYPE_SERVICE)
.setPattern("/(http[^#?]*)/([0-9a-fA-F]{11})",
TargetType.TYPE_ENTRY, "collection", "entry")
// external entry
.setPattern("/(http[^#?]*)", TargetType.TYPE_COLLECTION,
"collection")
// external feed
.setPattern("/([^/#?]+)/([^/#?]+)/([^/#?]+)(\\?[^#]*)?",
TargetType.TYPE_MEDIA, "collection", "entry",
"resource")
.setPattern("/([^/#?]+)/([^/#?]+)(\\?[^#]*)?",
TargetType.TYPE_ENTRY, "collection", "entry")
.setPattern("/([^/#?]+);categories",
TargetType.TYPE_CATEGORIES, "collection")
.setPattern("/([^/#?;]+)(\\?[^#]*)?",
TargetType.TYPE_COLLECTION, "collection")
.setPattern("/", TargetType.TYPE_COLLECTION);
super.setTargetResolver(resolver);
// url construction templates
setTargetBuilder(new TemplateTargetBuilder()
.setTemplate(TargetType.TYPE_SERVICE, "{target_base}")
.setTemplate(TargetType.TYPE_COLLECTION,
"{target_base}/{collection}{-opt|?|q,c,s,p,l,i,o}{-join|&|q,c,s,p,l,i,o}")
.setTemplate(TargetType.TYPE_CATEGORIES,
"{target_base}/{collection};categories")
.setTemplate(TargetType.TYPE_ENTRY,
"{target_base}/{collection}/{entry}"));
addWorkspace(this);
addFilter(new PaginationFilter());
addFilter(new OpenSearchFilter()
.setShortName("Trsst Search")
.setDescription("Search on entry metadata.")
.setTags("test", "example", "opensearch")
.setContact("admin@trsst.com")
.setTemplate(
"{target_base}/?q={searchTerms}&count={count?}&page={startPage?}&offset={startIndex?}&before={beforeDate?}&after={afterDate?}")
// .setTemplate(
// "{target_base}/?q={searchTerms}&c={count?}&s={startIndex?}&p={startPage?}&l={language?}&i={indexEncoding?}&o={outputEncoding?}")
.mapTargetParameter("q", "searchTerms")
.mapTargetParameter("count", "count")
.mapTargetParameter("before", "beforeDate")
.mapTargetParameter("after", "afterDates")
.mapTargetParameter("page", "startPage")
.mapTargetParameter("offset", "startIndex"));
// .mapTargetParameter("l", "language")
// .mapTargetParameter("i", "inputEncoding")
// .mapTargetParameter("o", "outputEncoding"));
}
public ResponseContext process(RequestContext request) {
Target target = request.getTarget();
if (target == null || target.getType() == TargetType.TYPE_NOT_FOUND) {
return ProviderHelper.notfound(request);
}
TargetType type = target.getType();
RequestProcessor processor = this.requestProcessors.get(type);
if (processor == null) {
return ProviderHelper.notfound(request);
}
WorkspaceManager wm = getWorkspaceManager(request);
CollectionAdapter adapter = wm.getCollectionAdapter(request);
Transactional transaction = adapter instanceof Transactional ? (Transactional) adapter
: null;
ResponseContext response = null;
try {
transactionStart(transaction, request);
response = processor.process(request, wm, adapter);
response = response != null ? response : processExtensionRequest(
request, adapter);
} catch (Throwable e) {
if (e instanceof ResponseContextException) {
ResponseContextException rce = (ResponseContextException) e;
if (rce.getStatusCode() >= 400 && rce.getStatusCode() < 500) {
// don't report routine 4xx HTTP errors
log.info("info: ", e);
} else {
log.error("inner error: ", e);
}
} else {
log.error("outer error: ", e);
}
transactionCompensate(transaction, request, e);
response = createErrorResponse(request, e);
return response;
} finally {
transactionEnd(transaction, request, response);
}
return response != null ? response : ProviderHelper.badrequest(request);
}
// @Override
// public ResponseContext process(RequestContext request) {
// try {
// log.info(request.getMethod().toString() + " "
// + request.getUri().toString());
// return super.process(request);
// } catch (Throwable t) {
// log.info(request.getMethod().toString() + " "
// + request.getUri().toString());
// log.error("Unexpected error: " + t);
// }
// return null;
// }
//
/**
* Returns null to function in containers with constrained permissions;
* Trsst servers generally don't need http security contexts.
*/
@Override
public Subject resolveSubject(RequestContext request) {
// return null to work in containers with constrained permissions
return null;
}
/**
* Override to return a custom storage instance. This implementation
* defaults to a single shared LuceneStorage instance.
*
* @param feedId
* a hint for implementors
* @return a Storage for the specified feed id
*/
protected Storage getStorage() {
if (sharedStorage == null) {
try {
Storage clientStorage = new FileStorage(Common.getClientRoot());
Storage cacheStorage = new FileStorage(Common.getServerRoot());
sharedStorage = new LuceneStorage(cacheStorage, clientStorage);
sharedStorage = new CachingStorage(sharedStorage);
} catch (IOException e) {
log.error("Could not initialize storage", e);
}
}
return sharedStorage;
}
/**
* Override to return a custom adapter instance. This implementation
* defaults to TrsstAdapter configured to use the result of
* getStorageFromFeedId.
*
* @param feedId
* a hint for implementors
* @return a TrsstAdapter for the specified feed id
*/
protected TrsstAdapter getAdapterForFeedId(String feedId)
throws IOException {
if (Common.ROOT_ALIAS.equals(feedId)) {
return new HomeAdapter(feedId, getStorage());
}
return new TrsstAdapter(feedId, getStorage());
}
private static Storage sharedStorage;
public CollectionAdapter getCollectionAdapter(RequestContext request) {
String feedId = request.getTarget().getParameter("collection");
if (feedId != null && feedId.trim().length() == 0) {
feedId = null;
}
if (feedId == null) {
// default to subdomain name if any
String host = request.getHeader("Host");
if (host != null) {
int i = host.lastIndexOf('.');
if (i != -1) {
host = host.substring(0, i);
i = host.lastIndexOf('.');
if (i != -1) {
host = host.substring(0, i);
i = host.lastIndexOf('.');
if (i != -1) {
feedId = host.substring(0, i);
} else {
feedId = host;
}
try {
Integer.parseInt(feedId);
// it's a numeric address, not a name.
feedId = null;
} catch (NumberFormatException nfe) {
// it's a hostname; continue.
}
}
}
}
}
if (feedId == null) {
feedId = Common.ROOT_ALIAS;
}
try {
TrsstAdapter result = idsToAdapters.get(feedId);
if (result == null) {
result = getAdapterForFeedId(feedId);
idsToAdapters.put(feedId, result);
}
return result;
} catch (Throwable t) {
log.error("Not found: id: " + feedId, t);
return null;
}
}
public class PaginationFilter implements Filter {
public ResponseContext filter(RequestContext request, FilterChain chain) {
RequestContextWrapper rcw = new RequestContextWrapper(request);
rcw.setAttribute("offset", 10);
rcw.setAttribute("count", 10);
return chain.next(rcw);
}
}
private final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(this
.getClass());
public String getTitle(RequestContext requsest) {
// workspace info title
return hostname;
}
/**
* Returns some or all of the most active feeds hosted on this server. This
* implementation calls Storage.getFeedIds(0,100). Override to think
* different.
*/
protected String[] getFeedIds(RequestContext request) {
// arbitrary cap: get up to most active hundred.
return getStorage().getFeedIds(0, 100);
}
public Collection<CollectionInfo> getCollections(RequestContext request) {
LinkedList<CollectionInfo> result = new LinkedList<CollectionInfo>();
Feed feed;
Parser parser = Abdera.getInstance().getParser();
CollectionInfo info;
Storage storage = getStorage();
for (String id : getFeedIds(request)) {
try {
feed = (Feed) parser.parse(
new StringReader(storage.readFeed(id))).getRoot();
String title = feed.getTitle();
// default title to id if null
if (title == null) {
title = id;
}
info = new SimpleCollectionInfo(title, id, "text/plain",
"text/html", "text/xml", "image/png", "image/jpeg",
"image/gif", "image/svg+xml", "video/mp4");
result.add(info);
} catch (ParseException e) {
log.warn("Could not parse collection info for feed: " + id
+ " : " + e.toString());
} catch (IOException e) {
log.warn("Could not read collection info for feed: " + id
+ " : " + e.toString());
}
}
return result;
}
public Workspace asWorkspaceElement(RequestContext request) {
Workspace workspace = request.getAbdera().getFactory().newWorkspace();
workspace.setTitle(getTitle(request));
for (CollectionInfo collection : getCollections(request))
workspace.addCollection(collection.asCollectionElement(request));
return workspace;
}
private static final class OrderedRegexTargetResolver extends
RegexTargetResolver {
// patterns is final in super
Map<Pattern, TargetType> orderedPatterns = new LinkedHashMap<Pattern, TargetType>();
// override to intercept pattern order
public RegexTargetResolver setPattern(String pattern, TargetType type,
String... fields) {
// ugh: don't call super so we don't compile twice
Pattern p = Pattern.compile(pattern);
orderedPatterns.put(p, type);
this.fields.put(p, fields);
return this;
}
// override to exclude BaseTargetPath (and now to use ordered patterns)
public Target resolve(Request request) {
RequestContext context = (RequestContext) request;
String uri = context.getTargetPath();
if (uri.startsWith(context.getTargetBasePath())) {
uri = uri.substring(context.getTargetBasePath().length());
}
// note: now first matching pattern wins
for (Pattern pattern : orderedPatterns.keySet()) {
Matcher matcher = pattern.matcher(uri);
if (matcher.lookingAt()) {
TargetType type = this.orderedPatterns.get(pattern);
String[] fields = this.fields.get(pattern);
return getTarget(type, context, matcher, fields);
}
}
return null;
}
};
}