/** * Licensed to The Apereo Foundation under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * * The Apereo Foundation licenses this file to you under the Educational * Community 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://opensource.org/licenses/ecl2.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.opencastproject.feed.impl; import org.opencastproject.feed.api.Feed; import org.opencastproject.feed.api.FeedGenerator; import org.opencastproject.security.api.Organization; import org.opencastproject.security.api.SecurityService; import org.opencastproject.util.doc.rest.RestParameter; import org.opencastproject.util.doc.rest.RestParameter.Type; import org.opencastproject.util.doc.rest.RestQuery; import org.opencastproject.util.doc.rest.RestResponse; import org.opencastproject.util.doc.rest.RestService; import com.rometools.rome.io.FeedException; import com.rometools.rome.io.SyndFeedOutput; import com.rometools.rome.io.WireFeedOutput; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Variant; @Path("/") @RestService(name = "feedservice", title = "Feed Service", abstractText = "This class is responsible of creating RSS and Atom feeds.", notes = "") /** * This class is responsible of creating RSS and Atom feeds. * <p> * The implementation relies on the request uri containing information about the requested feed type and the query used * to construct the feed contents. * </p> * <p> * Therefore, assuming that this servlet has been mounted to <code>/feeds/*</code>, a correct uri for this servlet looks * like this: <code>/feeds/<feed type>/<version>/<query></code>, e. g. * * <pre> * http://localhost/feeds/Atom/1.0/favorites * </pre> * * which would indicate a requeste to an atom 1.0 feed with <tt>favourites</tt> being the query. * * The servlet returns a HTTP status 200 with the feed data. * If the feed could not be found because the query is unknown a HTTP error 404 is returned * If the feed could not be build (wrong RSS or Atom version, corrupt data, etc) an HTTP error 500 is returned. */ public class FeedServiceImpl { /** The serial version uid */ private static final long serialVersionUID = -4623160106007127801L; /** Name of the size parameter */ private static final String PARAM_SIZE = "size"; /** Logging facility */ private static Logger logger = LoggerFactory.getLogger(FeedServiceImpl.class); /** List of feed generators */ private List<FeedGenerator> feeds = new ArrayList<FeedGenerator>(); /** The security service */ private SecurityService securityService = null; /* * Note: We're using Regex matching for the path here, instead of normal JAX-RS paths. Previously this class was a servlet, * which was fine except that it had auth issues. Removing the servlet fixed the auth issues, but then the paths (as written * in the RestQuery docs) don't work because JAX-RS does not support having "/" characters as part of the variable's value. * * So, what we've done instead is match everything that comes in under the /feeds/ namespace, and then substring it out the way * the old servlet code did. But without the servlet, or auth issues :) */ @GET @Produces(MediaType.TEXT_XML) @Path("{query: .*}") //FIXME: These Opencast REST classes do not support this path style, and need to have that support added @RestQuery(name = "getFeed", description = "Gets an Atom or RSS feed", pathParameters = { @RestParameter(description = "The feed type", name = "type", type = Type.STRING, isRequired = true), @RestParameter(description = "The feed version", name = "version", type = Type.STRING, isRequired = true), @RestParameter(description = "The feed query", name = "query", type = Type.STRING, isRequired = true) }, reponses = { @RestResponse(description = "Return the feed of the appropriate type", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR) }, returnDescription = "") public Response getFeed(@Context HttpServletRequest request) { String contentType = null; logger.debug("Requesting RSS or Atom feed."); FeedInfo feedInfo = null; Organization organization = securityService.getOrganization(); // Try to extract requested feed type and content try { feedInfo = extractFeedInfo(request); } catch (Exception e) { return Response.status(Status.BAD_REQUEST).build(); } // Set the content type if (feedInfo.getType().equals(Feed.Type.Atom)) contentType = "application/atom+xml"; else if (feedInfo.getType().equals(Feed.Type.RSS)) contentType = "application/rss+xml"; // Have a feed generator create the requested feed Feed feed = null; for (FeedGenerator generator : feeds) { if (generator.accept(feedInfo.getQuery())) { feed = generator.createFeed(feedInfo.getType(), feedInfo.getQuery(), feedInfo.getSize(), organization); if (feed == null) { return Response.serverError().build(); } break; } } // Have we found a feed generator? if (feed == null) { logger.debug("RSS/Atom feed could not be generated"); return Response.status(Status.NOT_FOUND).build(); } // Set character encoding Variant v = new Variant(MediaType.valueOf(contentType), null, feed.getEncoding()); String outputString = null; try { if (feedInfo.getType().equals(Feed.Type.RSS)) { logger.debug("Creating RSS feed output."); SyndFeedOutput output = new SyndFeedOutput(); outputString = output.outputString(new RomeRssFeed(feed, feedInfo)); } else { logger.debug("Creating Atom feed output."); WireFeedOutput output = new WireFeedOutput(); outputString = output.outputString(new RomeAtomFeed(feed, feedInfo)); } } catch (FeedException e) { return Response.serverError().build(); } return Response.ok(outputString, v).build(); } /** * Returns information about the requested feed by extracting all relevant pieces from the servlet request's uri. * <p> * This method throws an {@link IllegalStateException} if the information cannot be extracted from the uri. * </p> * * @param request * the servlet request * @return the requested feed * @throws IllegalStateException * if the uri does not contain sufficient information about the request */ private FeedInfo extractFeedInfo(HttpServletRequest request) throws IllegalStateException { String path = request.getPathInfo(); if (path.startsWith("/")) path = path.substring(1); String[] pathElements = path.split("/"); if (pathElements.length < 3) throw new IllegalStateException("Cannot extract requested feed parameters."); Feed.Type type = null; try { type = Feed.Type.parseString(pathElements[0]); } catch (Exception e) { throw new IllegalStateException("Cannot extract requested feed type."); } float version = 0; try { version = Float.parseFloat(pathElements[1]); } catch (NumberFormatException e) { throw new IllegalStateException("Cannot extract requested feed version."); } int queryLength = pathElements.length - 2; String[] query = new String[queryLength]; for (int i = 0; i < queryLength; i++) query[i] = pathElements[i + 2]; String sizeParam = request.getParameter(PARAM_SIZE); if (StringUtils.isNotBlank(sizeParam)) { try { return new FeedInfo(type, version, query, Integer.parseInt(sizeParam)); } catch (Exception e) { logger.warn("Value of feed parameter 'size' is not an integer: '{}'", sizeParam); return new FeedInfo(type, version, query); } } else { return new FeedInfo(type, version, query); } } /** * Adds the feed generator to the list of generators. * * @param generator * the generator */ public void addFeedGenerator(FeedGenerator generator) { logger.info("Registering '{}' feed", generator.getIdentifier()); feeds.add(generator); } /** * Removes the generator from the list of feed generators. * * @param generator * the feed generator */ public void removeFeedGenerator(FeedGenerator generator) { logger.info("Removing '{}' feed", generator.getIdentifier()); feeds.remove(generator); } /** * OSGi callback to set the security service. * * @param securityService * the security service */ void setSecurityService(SecurityService securityService) { this.securityService = securityService; } }