package org.atomhopper.mongodb.adapter; import java.io.StringReader; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLEncoder; import java.util.*; import org.apache.abdera.Abdera; import static org.apache.abdera.i18n.text.UrlEncoding.decode; import static org.apache.abdera.i18n.text.UrlEncoding.encode; import org.apache.abdera.model.Document; import org.apache.abdera.model.Entry; import org.apache.abdera.model.Feed; import org.apache.abdera.model.Link; import org.apache.commons.lang.StringUtils; import org.atomhopper.adapter.FeedInformation; import org.atomhopper.adapter.FeedSource; import org.atomhopper.adapter.NotImplemented; import org.atomhopper.adapter.ResponseBuilder; import org.atomhopper.adapter.AdapterHelper; import org.atomhopper.adapter.request.adapter.GetEntryRequest; import org.atomhopper.adapter.request.adapter.GetFeedRequest; import org.atomhopper.dbal.PageDirection; import static org.atomhopper.mongodb.adapter.MongodbUtilities.formatCollectionName; import org.atomhopper.mongodb.domain.PersistedEntry; import org.atomhopper.mongodb.query.CategoryCriteriaGenerator; import org.atomhopper.mongodb.query.SimpleCategoryCriteriaGenerator; import org.atomhopper.response.AdapterResponse; import org.atomhopper.util.uri.template.EnumKeyedTemplateParameters; import org.atomhopper.util.uri.template.URITemplate; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Order; import org.springframework.data.mongodb.core.query.Query; public class MongodbFeedSource implements FeedSource { private static final int PAGE_SIZE = 25; private static final String DATE_LAST_UPDATED = "dateLastUpdated"; private static final String FEED = "feed"; private static final String ID = "_id"; private MongoTemplate mongoTemplate; private AdapterHelper helper = new AdapterHelper(); public void setMongoTemplate(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } @Override @NotImplemented public void setParameters(Map<String, String> params) { throw new UnsupportedOperationException("Not supported yet."); } private void addFeedSelfLink(Feed feed, final String baseFeedUri, final GetFeedRequest getFeedRequest, final int pageSize, final String searchString) { StringBuilder queryParams = new StringBuilder(); boolean markerIsSet = false; queryParams.append(baseFeedUri).append("?limit=").append(String.valueOf(pageSize)); if (searchString.length() > 0) { queryParams.append("&search=").append(urlEncode(searchString).toString()); } if (getFeedRequest.getPageMarker() != null && getFeedRequest.getPageMarker().length() > 0) { queryParams.append("&marker=").append(getFeedRequest.getPageMarker()); markerIsSet = true; } if (markerIsSet) { queryParams.append("&direction=").append(getFeedRequest.getDirection()); } else { queryParams.append("&direction=backward"); if (queryParams.toString().equalsIgnoreCase(baseFeedUri + "?limit=" + pageSize + "&direction=backward")) { // They are calling the feedhead, just use the base feed uri // This keeps the validator at http://validator.w3.org/ happy queryParams.delete(0, queryParams.toString().length()).append(baseFeedUri); } } feed.addLink(queryParams.toString()).setRel(Link.REL_SELF); } private void addFeedCurrentLink(Feed hyrdatedFeed, final String baseFeedUri) { String url = helper.isArchived() ? helper.getCurrentUrl() : baseFeedUri; hyrdatedFeed.addLink( url, Link.REL_CURRENT); } private Feed hydrateFeed(Abdera abdera, List<PersistedEntry> persistedEntries, GetFeedRequest getFeedRequest, final int pageSize) { final Feed hydratedFeed = abdera.newFeed(); final String uuidUriScheme = "urn:uuid:"; final String baseFeedUri = decode(getFeedRequest.urlFor(new EnumKeyedTemplateParameters<URITemplate>(URITemplate.FEED))); final String searchString = getFeedRequest.getSearchQuery() != null ? getFeedRequest.getSearchQuery() : ""; if ( helper.isArchived() ) { helper.addArchiveNode( hydratedFeed ); } // Set the feed links addFeedCurrentLink(hydratedFeed, baseFeedUri); addFeedSelfLink(hydratedFeed, baseFeedUri, getFeedRequest, pageSize, searchString); PersistedEntry nextEntry = null; // TODO: We should have a link builder method for these if (!(persistedEntries.isEmpty())) { hydratedFeed.setId( uuidUriScheme + UUID.randomUUID().toString() ); hydratedFeed.setTitle( persistedEntries.get( 0 ).getFeed() ); // Set the previous link hydratedFeed.addLink( new StringBuilder() .append( baseFeedUri ).append( "?marker=" ) .append( persistedEntries.get( 0 ).getEntryId() ) .append( "&limit=" ).append( String.valueOf( pageSize ) ) .append( "&search=" ).append( urlEncode( searchString ).toString() ) .append( "&direction=forward" ).toString() ) .setRel( helper.getPrevLink() ); final PersistedEntry lastEntryInCollection = persistedEntries.get(persistedEntries.size() - 1); Query nextLinkQuery = new Query(Criteria.where(FEED).is(lastEntryInCollection.getFeed())).limit(1).addCriteria(Criteria.where(DATE_LAST_UPDATED).lt(lastEntryInCollection.getDateLastUpdated())); nextLinkQuery.sort().on(DATE_LAST_UPDATED, Order.DESCENDING); SimpleCategoryCriteriaGenerator simpleCategoryCriteriaGenerator = new SimpleCategoryCriteriaGenerator(searchString); simpleCategoryCriteriaGenerator.enhanceCriteria(nextLinkQuery); nextEntry = mongoTemplate.findOne(nextLinkQuery, PersistedEntry.class, formatCollectionName(lastEntryInCollection.getFeed())); if (nextEntry != null) { // Set the next link hydratedFeed.addLink( new StringBuilder().append( baseFeedUri ) .append( "?marker=" ).append( nextEntry.getEntryId() ) .append( "&limit=" ).append( String.valueOf( pageSize ) ) .append( "&search=" ).append( urlEncode( searchString ).toString() ) .append( "&direction=backward" ).toString() ) .setRel( helper.getNextLink() ); } } // if we are at the last page & there is an archive link, provide it if ( nextEntry == null && helper.getArchiveUrl() != null ) { hydratedFeed.addLink(new StringBuilder().append( helper.getArchiveUrl() ).append( "?limit=" ).append(String.valueOf(pageSize)) .append( "&direction=backward" ).toString()) .setRel( FeedSource.REL_ARCHIVE_NEXT ); } for (PersistedEntry persistedFeedEntry : persistedEntries) { hydratedFeed.addEntry( hydrateEntry( persistedFeedEntry, abdera ) ); } return hydratedFeed; } private Entry hydrateEntry(PersistedEntry persistedEntry, Abdera abderaReference) { final Document<Entry> hydratedEntryDocument = abderaReference.getParser().parse(new StringReader(persistedEntry.getEntryBody())); Entry entry = null; if (hydratedEntryDocument != null) { entry = hydratedEntryDocument.getRoot(); entry.setUpdated(persistedEntry.getDateLastUpdated()); entry.setPublished(persistedEntry.getCreationDate()); } return entry; } @Override public AdapterResponse<Entry> getEntry(GetEntryRequest getEntryRequest) { final PersistedEntry entry = mongoTemplate.findOne(new Query( Criteria.where(ID).is(getEntryRequest.getEntryId())), PersistedEntry.class, formatCollectionName(getEntryRequest.getFeedName())); AdapterResponse<Entry> response = ResponseBuilder.notFound(); if (entry != null) { response = ResponseBuilder.found(hydrateEntry(entry, getEntryRequest.getAbdera())); } return response; } @Override public void setArchiveUrl( URL url ) { helper.setArchiveUrl( url ); } @Override public void setCurrentUrl( URL urlCurrent ) { helper.setCurrentUrl( urlCurrent ); } @Override public AdapterResponse<Feed> getFeed(GetFeedRequest getFeedRequest) { AdapterResponse<Feed> response; int pageSize = PAGE_SIZE; final String pageSizeString = getFeedRequest.getPageSize(); if (StringUtils.isNotBlank(pageSizeString)) { pageSize = Integer.parseInt(pageSizeString); } final String marker = getFeedRequest.getPageMarker(); if (StringUtils.isNotBlank(marker)) { response = getFeedPage(getFeedRequest, marker, pageSize); } else { response = getFeedHead(getFeedRequest, pageSize); } return response; } private AdapterResponse<Feed> getFeedHead(GetFeedRequest getFeedRequest, int pageSize) { final Abdera abdera = getFeedRequest.getAbdera(); Query queryIfFeedExists = new Query(Criteria.where(FEED).is(getFeedRequest.getFeedName())); final PersistedEntry persistedEntry = mongoTemplate.findOne(queryIfFeedExists, PersistedEntry.class, formatCollectionName(getFeedRequest.getFeedName())); AdapterResponse<Feed> response = null; if (persistedEntry != null) { final String searchString = getFeedRequest.getSearchQuery() != null ? getFeedRequest.getSearchQuery() : ""; Query queryForFeedHead = new Query(Criteria.where(FEED).is(getFeedRequest.getFeedName())).limit(pageSize); queryForFeedHead.sort().on(DATE_LAST_UPDATED, Order.DESCENDING); SimpleCategoryCriteriaGenerator simpleCategoryCriteriaGenerator = new SimpleCategoryCriteriaGenerator(searchString); simpleCategoryCriteriaGenerator.enhanceCriteria(queryForFeedHead); final List<PersistedEntry> persistedEntries = mongoTemplate.find(queryForFeedHead, PersistedEntry.class, formatCollectionName(getFeedRequest.getFeedName())); Feed hyrdatedFeed = hydrateFeed(abdera, persistedEntries, getFeedRequest, pageSize); // Set the last link in the feed head final String baseFeedUri = decode(getFeedRequest.urlFor(new EnumKeyedTemplateParameters<URITemplate>(URITemplate.FEED))); Query feedQuery = new Query(Criteria.where(FEED).is(getFeedRequest.getFeedName())); simpleCategoryCriteriaGenerator.enhanceCriteria(feedQuery); Query lastLinkQuery = new Query(Criteria.where(FEED).is(getFeedRequest.getFeedName())).limit(pageSize); simpleCategoryCriteriaGenerator.enhanceCriteria(lastLinkQuery); lastLinkQuery.sort().on(DATE_LAST_UPDATED, Order.ASCENDING); final List<PersistedEntry> lastPersistedEntries = mongoTemplate.find(lastLinkQuery, PersistedEntry.class, formatCollectionName(getFeedRequest.getFeedName())); if (!helper.isArchived() && lastPersistedEntries != null && !(lastPersistedEntries.isEmpty())) { hyrdatedFeed.addLink(new StringBuilder().append(baseFeedUri).append("?marker=").append(lastPersistedEntries.get(lastPersistedEntries.size() - 1).getEntryId()).append("&limit=").append(String.valueOf(pageSize)).append("&search=").append(urlEncode(searchString).toString()).append("&direction=backward").toString()).setRel(Link.REL_LAST); } response = ResponseBuilder.found(hyrdatedFeed); } return response != null ? response : ResponseBuilder.found(abdera.newFeed()); } private AdapterResponse<Feed> getFeedPage(GetFeedRequest getFeedRequest, String marker, int pageSize) { AdapterResponse<Feed> response; PageDirection pageDirection; try { final String pageDirectionValue = getFeedRequest.getDirection(); pageDirection = PageDirection.valueOf(pageDirectionValue.toUpperCase()); } catch (Exception iae) { return ResponseBuilder.badRequest("Marker must have a page direction specified as either \"forward\" or \"backward\""); } final PersistedEntry markerEntry = mongoTemplate.findOne(new Query( Criteria.where(FEED).is(getFeedRequest.getFeedName()).andOperator(Criteria.where(ID).is(marker))), PersistedEntry.class, formatCollectionName(getFeedRequest.getFeedName())); if (markerEntry != null) { final String searchString = getFeedRequest.getSearchQuery() != null ? getFeedRequest.getSearchQuery() : ""; final Feed feed = hydrateFeed( getFeedRequest.getAbdera(), enhancedGetFeedPage( getFeedRequest.getFeedName(), markerEntry, pageDirection, new SimpleCategoryCriteriaGenerator(searchString), pageSize), getFeedRequest, pageSize); response = ResponseBuilder.found(feed); } else { response = ResponseBuilder.notFound("No entry with specified marker found"); } return response; } private List<PersistedEntry> enhancedGetFeedPage(final String feedName, final PersistedEntry markerEntry, final PageDirection direction, final CategoryCriteriaGenerator criteriaGenerator, final int pageSize) { final LinkedList<PersistedEntry> feedPage = new LinkedList<PersistedEntry>(); final Query query = new Query(Criteria.where(FEED).is(feedName)).limit(pageSize); criteriaGenerator.enhanceCriteria(query); switch (direction) { case FORWARD: query.addCriteria(Criteria.where(DATE_LAST_UPDATED).gt(markerEntry.getCreationDate())); query.sort().on(DATE_LAST_UPDATED, Order.ASCENDING); feedPage.addAll(mongoTemplate.find(query, PersistedEntry.class, formatCollectionName(feedName))); Collections.reverse(feedPage); break; case BACKWARD: query.addCriteria(Criteria.where(DATE_LAST_UPDATED).lte(markerEntry.getCreationDate())); query.sort().on(DATE_LAST_UPDATED, Order.DESCENDING); feedPage.addAll(mongoTemplate.find(query, PersistedEntry.class, formatCollectionName(feedName))); break; } return feedPage; } @Override public FeedInformation getFeedInformation() { throw new UnsupportedOperationException("Not supported yet."); } private String urlEncode(String searchString) { try { return URLEncoder.encode(searchString, "UTF-8"); } catch (UnsupportedEncodingException e) { //noop - should never get here return ""; } } }