| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. The ASF licenses this file to You |
| * 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. For additional information regarding |
| * copyright in this work, please see the NOTICE file in the top level |
| * directory of this distribution. |
| */ |
| package org.apache.abdera.protocol.server; |
| |
| import java.io.IOException; |
| import java.util.Arrays; |
| import java.util.Comparator; |
| import java.util.Date; |
| import java.util.List; |
| |
| import javax.activation.MimeType; |
| import javax.xml.namespace.QName; |
| |
| import org.apache.abdera.Abdera; |
| import org.apache.abdera.i18n.iri.IRI; |
| import org.apache.abdera.i18n.text.Localizer; |
| import org.apache.abdera.i18n.text.Sanitizer; |
| import org.apache.abdera.model.AtomDate; |
| import org.apache.abdera.model.Base; |
| import org.apache.abdera.model.Content; |
| import org.apache.abdera.model.Document; |
| import org.apache.abdera.model.Element; |
| import org.apache.abdera.model.Entry; |
| import org.apache.abdera.model.ExtensibleElement; |
| import org.apache.abdera.model.Feed; |
| import org.apache.abdera.model.Link; |
| import org.apache.abdera.protocol.error.Error; |
| import org.apache.abdera.protocol.server.context.AbstractResponseContext; |
| import org.apache.abdera.protocol.server.context.BaseResponseContext; |
| import org.apache.abdera.protocol.server.context.EmptyResponseContext; |
| import org.apache.abdera.protocol.server.context.StreamWriterResponseContext; |
| import org.apache.abdera.util.Constants; |
| import org.apache.abdera.util.EntityTag; |
| import org.apache.abdera.util.MimeTypeHelper; |
| import org.apache.abdera.writer.NamedWriter; |
| import org.apache.abdera.writer.StreamWriter; |
| import org.apache.abdera.writer.WriterFactory; |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| |
| /** |
| * Provides support methods for {@link Provider} |
| */ |
| public class ProviderHelper { |
| private final static Log log = |
| LogFactory.getLog(ProviderHelper.class); |
| |
| private ProviderHelper() {} |
| |
| public static int getPageSize( |
| RequestContext request, |
| String pagesizeparam, |
| int defaultpagesize) { |
| int size = defaultpagesize; |
| try { |
| String _ps = request.getParameter(pagesizeparam); |
| size = (_ps != null) ? |
| Math.min( |
| Math.max( |
| Integer.parseInt(_ps),0), |
| defaultpagesize) : |
| defaultpagesize; |
| } catch (Exception e) {} |
| log.debug(Localizer.sprintf("PAGE.SIZE",size)); |
| return size; |
| } |
| |
| public static int getOffset( |
| RequestContext request, |
| String pageparam, |
| int pageSize) { |
| int offset = 0; |
| try { |
| String _page = request.getParameter(pageparam); |
| int page =(_page != null) ? Integer.parseInt(_page) : 1; |
| page = Math.max(page, 1) - 1; |
| offset = pageSize * page; |
| } catch (Exception e) {} |
| log.debug(Localizer.sprintf("OFFSET",offset)); |
| return offset; |
| } |
| |
| /** |
| * Returns an Error document based on the StreamWriter |
| */ |
| public static AbstractResponseContext createErrorResponse( |
| Abdera abdera, |
| final int code, |
| final String message) { |
| return createErrorResponse(abdera,code,message,null); |
| } |
| |
| /** |
| * Returns an Error document based on the StreamWriter |
| */ |
| public static AbstractResponseContext createErrorResponse( |
| Abdera abdera, |
| final int code, |
| final String message, |
| final Throwable t) { |
| AbstractResponseContext rc = |
| new StreamWriterResponseContext(abdera) { |
| protected void writeTo(StreamWriter sw) |
| throws IOException { |
| Error.create(sw, code, message, t); |
| } |
| }; |
| rc.setStatus(code); |
| rc.setStatusText(message); |
| return rc; |
| } |
| |
| /** |
| * Return a server error |
| */ |
| public static ResponseContext servererror( |
| RequestContext request, |
| String reason, |
| Throwable t) { |
| log.info(Localizer.get("SERVER_ERROR"), t); |
| return createErrorResponse(request.getAbdera(),500,reason,t); |
| } |
| |
| /** |
| * Return a server error |
| */ |
| public static ResponseContext servererror( |
| RequestContext request, |
| Throwable t) { |
| return servererror(request,"Server Error",t); |
| } |
| |
| /** |
| * Return an unauthorized error |
| */ |
| public static ResponseContext unauthorized( |
| RequestContext request, |
| String reason) { |
| log.debug(Localizer.get("UNAUTHORIZED")); |
| return createErrorResponse(request.getAbdera(),401,reason); |
| } |
| |
| public static ResponseContext unauthorized( |
| RequestContext request) { |
| return unauthorized(request, "Unauthorized"); |
| } |
| |
| /** |
| * Return an unauthorized error |
| */ |
| public static ResponseContext forbidden( |
| RequestContext request, |
| String reason) { |
| log.debug(Localizer.get("FORBIDDEN")); |
| return createErrorResponse(request.getAbdera(),403,reason); |
| } |
| |
| public static ResponseContext forbidden( |
| RequestContext request) { |
| return forbidden(request, "Forbidden"); |
| } |
| |
| /** |
| * Return a 204 No Content response |
| */ |
| public static ResponseContext nocontent(String reason) { |
| return new EmptyResponseContext(204,reason); |
| } |
| |
| public static ResponseContext nocontent() { |
| return nocontent("Not Content"); |
| } |
| |
| /** |
| * Return a 404 not found error |
| */ |
| public static ResponseContext notfound( |
| RequestContext request, |
| String reason) { |
| log.debug(Localizer.get("UNKNOWN")); |
| return createErrorResponse(request.getAbdera(),404,reason); |
| } |
| |
| public static ResponseContext notfound( |
| RequestContext request) { |
| return notfound(request, "Not Found"); |
| } |
| |
| /** |
| * Return a 405 method not allowed error |
| */ |
| public static ResponseContext notallowed( |
| RequestContext request, |
| String reason, |
| String... methods) { |
| log.debug(Localizer.get("NOT.ALLOWED")); |
| AbstractResponseContext resp = |
| createErrorResponse(request.getAbdera(),405,reason); |
| resp.setAllow(methods); |
| return resp; |
| } |
| |
| public static ResponseContext notallowed( |
| RequestContext request, |
| String... methods) { |
| return notallowed(request, "Method Not Allowed", methods); |
| } |
| |
| public static ResponseContext notallowed( |
| RequestContext request) { |
| return notallowed(request, getDefaultMethods(request)); |
| } |
| |
| /** |
| * Return a 400 bad request error |
| */ |
| public static ResponseContext badrequest( |
| RequestContext request, |
| String reason) { |
| log.debug(Localizer.get("BAD.REQUEST")); |
| return createErrorResponse(request.getAbdera(),400,reason); |
| } |
| |
| public static ResponseContext badrequest( |
| RequestContext request) { |
| return badrequest(request, "Bad Request"); |
| } |
| |
| /** |
| * Return a 409 conflict error |
| */ |
| public static ResponseContext conflict( |
| RequestContext request, |
| String reason) { |
| log.debug(Localizer.get("CONFLICT")); |
| return createErrorResponse(request.getAbdera(),409,reason); |
| } |
| |
| public static ResponseContext conflict( |
| RequestContext request) { |
| return conflict(request, "Conflict"); |
| } |
| |
| /** |
| * Return a service unavailable error |
| */ |
| public static ResponseContext unavailable( |
| RequestContext request, |
| String reason) { |
| log.debug(Localizer.get("UNAVAILABLE")); |
| return createErrorResponse(request.getAbdera(),503,reason); |
| } |
| |
| public static ResponseContext unavailable( |
| RequestContext request) { |
| return unavailable(request, "Service Unavailable"); |
| } |
| |
| public static ResponseContext notmodified( |
| RequestContext request, |
| String reason) { |
| log.debug(Localizer.get("NOT.MODIFIED")); |
| return new EmptyResponseContext(304,reason); |
| } |
| |
| public static ResponseContext notmodified( |
| RequestContext request) { |
| return notmodified(request, "Not Modified"); |
| } |
| |
| public static ResponseContext preconditionfailed( |
| RequestContext request, |
| String reason) { |
| log.debug(Localizer.get("PRECONDITION.FAILED")); |
| return createErrorResponse(request.getAbdera(),412,reason); |
| } |
| |
| public static ResponseContext preconditionfailed( |
| RequestContext request) { |
| return preconditionfailed(request, "Precondition Failed"); |
| } |
| |
| /** |
| * Return a 415 media type not-supported error |
| */ |
| public static ResponseContext notsupported( |
| RequestContext request, |
| String reason) { |
| log.debug(Localizer.get("NOT.SUPPORTED")); |
| return createErrorResponse(request.getAbdera(),415,reason); |
| } |
| |
| public static ResponseContext notsupported( |
| RequestContext request) { |
| return notsupported(request, "Media Type Not Supported"); |
| } |
| |
| /** |
| * Return a 423 locked error |
| */ |
| public static ResponseContext locked( |
| RequestContext request, |
| String reason) { |
| log.debug(Localizer.get("LOCKED")); |
| return createErrorResponse(request.getAbdera(),423,reason); |
| } |
| |
| public static ResponseContext locked( |
| RequestContext request) { |
| return locked(request, "Locked"); |
| } |
| |
| /** |
| * Return a document |
| */ |
| @SuppressWarnings("unchecked") |
| public static ResponseContext returnBase( |
| Base base, |
| int status, |
| Date lastModified) { |
| log.debug(Localizer.get("RETURNING.DOCUMENT")); |
| BaseResponseContext response = new BaseResponseContext(base); |
| response.setStatus(status); |
| if (lastModified != null) response.setLastModified(lastModified); |
| response.setContentType(MimeTypeHelper.getMimeType(base)); |
| Document doc = base instanceof Document ? (Document)base : ((Element)base).getDocument(); |
| if (doc.getEntityTag() != null) { |
| response.setEntityTag(doc.getEntityTag()); |
| } else if (doc.getLastModified() != null) { |
| response.setLastModified(doc.getLastModified()); |
| } |
| return response; |
| } |
| |
| /** |
| * Sanitize the value of a Slug header. Any non alphanumeric characters in |
| * the slug are replaced with an underscore |
| */ |
| public static String sanitizeSlug(String slug) { |
| if (slug == null) throw new IllegalArgumentException(Localizer.get("SLUG.NOT.NULL")); |
| String sanitized = Sanitizer.sanitize(slug); |
| log.debug(Localizer.sprintf("SLUG.SANITIZED", slug, sanitized)); |
| return sanitized; |
| } |
| |
| /** |
| * Check to see if the entry is minimally valid according to RFC4287. |
| * This is not a complete check. It just verifies that the appropriate |
| * elements are present and that their values can be accessed. |
| */ |
| public static boolean isValidEntry(Entry entry) { |
| try { |
| IRI id = entry.getId(); |
| if (id == null || |
| id.toString().trim().length() == 0 || |
| !id.isAbsolute()) return false; |
| if (entry.getTitle() == null) return false; |
| if (entry.getUpdated() == null) return false; |
| if (entry.getAuthor() == null && |
| (entry.getSource() != null && |
| entry.getAuthor() == null)) return false; |
| Content content = entry.getContentElement(); |
| if (content == null) { |
| if (entry.getAlternateLink() == null) return false; |
| } else { |
| if ((content.getSrc() != null || |
| content.getContentType() == Content.Type.MEDIA) && |
| entry.getSummaryElement() == null) { |
| log.debug(Localizer.sprintf("CHECKING.VALID.ENTRY",false)); |
| return false; |
| } |
| } |
| } catch (Exception e) { |
| log.debug(Localizer.sprintf("CHECKING.VALID.ENTRY",false)); |
| return false; |
| } |
| log.debug(Localizer.sprintf("CHECKING.VALID.ENTRY",true)); |
| return true; |
| } |
| |
| /** |
| * Return false if the element contains any extension elements that are not |
| * supported |
| */ |
| public static boolean checkElementNamespaces( |
| Element element, |
| List<String> ignore) { |
| List<QName> attrs = element.getExtensionAttributes(); |
| for (QName qname : attrs) { |
| String ns = qname.getNamespaceURI(); |
| if (!ignore.contains(ns)) return false; |
| } |
| if (element instanceof ExtensibleElement) { |
| ExtensibleElement ext = (ExtensibleElement) element; |
| List<Element> extensions = ext.getExtensions(); |
| for (Element el : extensions) { |
| QName qname = el.getQName(); |
| String ns = qname.getNamespaceURI(); |
| if (!ignore.contains(ns)) return false; |
| if (!checkElementNamespaces(el, ignore)) return false; |
| } |
| } |
| return true; |
| } |
| |
| public static boolean beforeOrEqual(Date d1, Date d2) { |
| long l1 = d1.getTime() / 1000; // drop milliseconds |
| long l2 = d2.getTime() / 1000; // drop milliseconds |
| return l1 <= l2; |
| } |
| |
| public static IRI resolveBase(RequestContext request) { |
| return request.getBaseUri().resolve(request.getUri()); |
| } |
| |
| public static String combine(String... vals) { |
| StringBuilder buf = new StringBuilder(); |
| for(String val : vals) { |
| if (buf.length() > 0) buf.append(", "); |
| buf.append(val); |
| } |
| return buf.toString(); |
| } |
| |
| public static String[] getDefaultMethods(RequestContext request) { |
| TargetType type = request.getTarget().getType(); |
| if (type == null) return new String[0]; |
| if (type == TargetType.TYPE_COLLECTION) return new String[] { "GET", "HEAD", "OPTIONS", "POST" }; |
| if (type == TargetType.TYPE_CATEGORIES) return new String[] { "GET", "HEAD", "OPTIONS" }; |
| if (type == TargetType.TYPE_ENTRY) return new String[] { "DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT" }; |
| if (type == TargetType.TYPE_MEDIA) return new String[] { "DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT" }; |
| if (type == TargetType.TYPE_SERVICE) return new String[] { "GET", "HEAD", "OPTIONS" }; |
| return new String[] { "GET", "HEAD", "OPTIONS" }; |
| } |
| |
| public static boolean defaultCheckMethod( |
| RequestContext request, |
| String[] methods) { |
| return (java.util.Arrays.binarySearch(methods, request.getMethod()) >= 0); |
| } |
| |
| public static boolean isAtom(RequestContext request) { |
| MimeType mt = request.getContentType(); |
| String ctype = (mt != null) ? mt.toString() : null; |
| return ctype != null && MimeTypeHelper.isAtom(ctype); |
| } |
| |
| private static class QTokenComparator |
| implements Comparator<QToken> { |
| public int compare(QToken o1, QToken o2) { |
| if (o1.qvalue > o2.qvalue) |
| return -1; |
| if (o1.qvalue < o2.qvalue) |
| return 1; |
| return 0; |
| } |
| } |
| |
| private static class QToken { |
| String token; |
| double qvalue = 1.0; |
| QToken(String token,double qvalue) { |
| this.token = token; |
| this.qvalue = qvalue; |
| } |
| } |
| |
| /** |
| * <p>Utility method for parsing HTTP content negotiation headers and sorting |
| * their tokens according to their q parameter values.</p> |
| * |
| * <p>e.g. Accept: audio/*; q=0.2, audio/basic, audio/mpeg; q=0.1</p> |
| * |
| * <p>would sort into:</p> |
| * <pre> |
| * audio/basic |
| * audio/* |
| * audio/mpeg |
| * </pre> |
| */ |
| public static String[] orderByQ(String header) { |
| if (header == null || header.length() == 0) return new String[0]; |
| String[] tokens = header.split(","); |
| QToken[] qtokens = new QToken[tokens.length]; |
| for (int i = 0; i < tokens.length; i++) { |
| String token = tokens[i]; |
| String[] qvalues = token.trim().split(";"); |
| String t = qvalues[0]; |
| if (qvalues.length > 1) { |
| for (int n = 1; n < qvalues.length; n++) { |
| String[] v = qvalues[n].trim().split("="); |
| if (v[0].trim().equals("q")) { |
| double qv = Double.parseDouble(v[1]); |
| qtokens[i] = new QToken(t,qv); |
| break; |
| } |
| } |
| } |
| if (qtokens[i] == null) qtokens[i] = new QToken(t,1.0); |
| } |
| Arrays.sort(qtokens, new QTokenComparator()); |
| tokens = new String[qtokens.length]; |
| for (int n = 0; n < qtokens.length; n++) { |
| tokens[n] = qtokens[n].token; |
| } |
| return tokens; |
| } |
| |
| /** |
| * Returns an appropriate NamedWriter instance given an appropriately |
| * formatted HTTP Accept header. The header will be parsed and sorted |
| * according to it's q parameter values. The first named writer capable |
| * of supporting the specified type, in order of q-value preference, will |
| * be returned. The results on this are not always predictable. For instance, |
| * if the Accept header says "application/*" it could end up with either the |
| * JSON writer or the PrettyXML writer, or any other writer that supports |
| * any writer that supports a specific form of "application/*". It's always |
| * best to be very specific in the Accept headers. |
| */ |
| public static NamedWriter getAcceptableNamedWriter( |
| Abdera abdera, String accept_header) { |
| String[] sorted_accepts = orderByQ(accept_header); |
| WriterFactory factory = abdera.getWriterFactory(); |
| if (factory == null) return null; |
| for (String accept : sorted_accepts) { |
| NamedWriter writer = (NamedWriter) factory.getWriterByMediaType(accept); |
| if (writer != null) return writer; |
| } |
| return null; |
| } |
| |
| public static NamedWriter getNamedWriter(Abdera abdera, String mediatype) { |
| WriterFactory factory = abdera.getWriterFactory(); |
| if (factory == null) return null; |
| NamedWriter writer = (NamedWriter) factory.getWriterByMediaType(mediatype); |
| return writer; |
| } |
| |
| public static EntityTag calculateEntityTag(Base base) { |
| String id = null; |
| String modified = null; |
| if (base instanceof Entry) { |
| Entry entry = (Entry) base; |
| id = entry.getId().toString(); |
| modified = |
| AtomDate.format( |
| entry.getEdited() != null ? |
| entry.getEdited() : |
| entry.getUpdated() |
| ); |
| } else if (base instanceof Feed) { |
| Feed feed = (Feed) base; |
| id = feed.getId().toString(); |
| modified = AtomDate.format(feed.getUpdated()); |
| } else if (base instanceof Document) { |
| return calculateEntityTag(((Document<?>)base).getRoot()); |
| } |
| return EntityTag.generate(id, modified); |
| } |
| |
| public static String getEditUriFromEntry( |
| Entry entry) { |
| String editUri = null; |
| List<Link> editLinks = entry.getLinks("edit"); |
| if (editLinks != null) { |
| for (Link link : editLinks) { |
| // if there is more than one edit link, we should not automatically |
| // assume that it's always going to point to an Atom document |
| // representation. |
| if (link.getMimeType() != null) { |
| if (MimeTypeHelper.isMatch( |
| link.getMimeType().toString(), |
| Constants.ATOM_MEDIA_TYPE)) { |
| editUri = link.getResolvedHref().toString(); |
| break; |
| } |
| } else { |
| // edit link with no type attribute is the right one to use |
| editUri = link.getResolvedHref().toString(); |
| break; |
| } |
| } |
| } |
| return editUri; |
| } |
| |
| public static String[] getAcceptableTypes( |
| RequestContext request) { |
| String accept = request.getAccept(); |
| return orderByQ(accept); |
| } |
| |
| public static boolean isPreferred( |
| RequestContext request, |
| String s1, |
| String s2) { |
| return isPreferred( |
| getAcceptableTypes(request), |
| s1,s2); |
| } |
| |
| public static boolean isPreferred( |
| String[] accepts, |
| String s1, |
| String s2) { |
| int i1 = accepts.length, i2 = accepts.length; |
| for (int n = 0; n < accepts.length; n++) { |
| if (MimeTypeHelper.isMatch(s1, accepts[n])) { |
| i1 = n; |
| break; |
| } |
| } |
| for (int n = 0; n < accepts.length; n++) { |
| if (MimeTypeHelper.isMatch(s2, accepts[n])) { |
| i2 = n; |
| break; |
| } |
| } |
| return i1 < i2; |
| } |
| } |