/*
* Copyright (c) 2007-2014 by Public Library of Science
*
* http://plos.org
* http://ambraproject.org
*
* 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 org.ambraproject.service.feed;
import org.ambraproject.ApplicationException;
import org.ambraproject.models.Journal;
import org.ambraproject.service.annotation.AnnotationService;
import org.ambraproject.service.article.BrowseService;
import org.ambraproject.service.hibernate.HibernateServiceImpl;
import org.ambraproject.service.journal.JournalService;
import org.ambraproject.service.search.SolrException;
import org.ambraproject.service.search.SolrFieldConversion;
import org.ambraproject.service.search.SolrHttpService;
import org.ambraproject.service.trackback.TrackbackService;
import org.ambraproject.views.AnnotationView;
import org.ambraproject.views.LinkbackView;
import org.ambraproject.views.TOCArticle;
import org.ambraproject.views.TOCArticleGroup;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.HierarchicalConfiguration;
import org.apache.solr.client.solrj.SolrQuery;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.transaction.annotation.Transactional;
import org.w3c.dom.Document;
import java.net.URISyntaxException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* The <code>FeedService</code> supplies the API for querying feed requests. <code>FeedService</code> is a Spring
* injected singleton which coordinates access to the <code>annotationService and articleService</code>
*/
public class FeedServiceImpl extends HibernateServiceImpl implements FeedService {
private static final Logger log = LoggerFactory.getLogger(FeedServiceImpl.class);
private AnnotationService annotationService; // Annotation service Spring injected.
private TrackbackService trackbackService; // Trackback service Spring injected
private BrowseService browseService; // Browse Article Servcie Spring Injected
private JournalService journalService; // Journal service Spring injected.
private SolrHttpService solrHttpService; // solr service
private Configuration configuration;
private SolrFieldConversion solrFieldConverter;
private int queryTimeout = 60000;
private Map validSorts = null;
private List displaySorts = null;
private Map validKeywords = null;
private static final Pattern SORT_OPTION_PATTERN = Pattern.compile(",(?![^\\(\\)]*\\))");
/**
* Constructor - currently does nothing.
*/
public FeedServiceImpl() {
}
/**
* Creates and returns a new <code>Key</code> for clients of FeedService.
*
* @return Key a new FeedSearchParameters to be used as a data model for the FeedAction.
*/
@Override
public FeedSearchParameters newSearchParameters() {
return new FeedSearchParameters();
}
/**
* Queries for a list of articles from solr using the parameters set in searchParams
*
* @param searchParameters
* @return solr search result that contains list of articles
*/
@Override
public Document getArticles(final FeedSearchParameters searchParameters) {
Map<String, String> params = new HashMap<String, String>();
// result format
params.put("wt", "xml");
// what I want returned, the fields needed for rss feed
params.put("fl", "id,title_display,publication_date,author_without_collab_display,author_collab_only_display," +
"author_display,volume,issue,article_type,subject_hierarchy,abstract_primary_display,copyright");
// filters
String fq = "doc_type:full " +
"AND !article_type_facet:\"Issue Image\" " +
"AND cross_published_journal_key:" + searchParameters.getJournal();
String[] categories = searchParameters.getCategories();
if (categories != null && categories.length > 0) {
StringBuffer sb = new StringBuffer();
for (String category : categories) {
sb.append("\"").append(category).append("\" AND ");
}
params.put("q", "subject_level_1:(" + sb.substring(0, sb.length() - 5) + ")");
}
if (searchParameters.getAuthor() != null) {
fq = fq + " AND author:\"" + searchParameters.getAuthor() + "\"";
}
String startDate = "*";
String endDate = "*";
boolean addDateRange = false;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
if (searchParameters.getSDate() != null) {
startDate = sdf.format(searchParameters.getSDate().getTime());
startDate = startDate + "T00:00:00Z";
addDateRange = true;
}
if (searchParameters.getEDate() != null) {
endDate = sdf.format(searchParameters.getEDate().getTime());
endDate = endDate + "T00:00:00Z";
addDateRange = true;
}
if (addDateRange == true) {
fq = fq + " AND publication_date:[" + startDate + " TO " + endDate + "]";
}
params.put("fq", fq);
// number of results
params.put("rows", Integer.toString(searchParameters.getMaxResults()));
// sort the result
if (searchParameters.isMostViewed()) {
// Sorts RSS Feed for the most viewed articles linked from the most viewed tab.
String mostViewedKey = "ambra.virtualJournals." + journalService.getCurrentJournalName() + ".mostViewedArticles";
Integer days = configuration.getInt(mostViewedKey + ".timeFrame");
String sortField = (days != null) ? solrFieldConverter.getViewCountingFieldName(days)
: solrFieldConverter.getAllTimeViewsField();
params.put("sort", sortField + " desc");
} else {
params.put("sort", "publication_date desc");
}
Document result = null;
try {
result = solrHttpService.makeSolrRequest(params);
} catch (SolrException e) {
e.printStackTrace();
}
return result;
}
@Override
public Document getSearchArticles(final FeedSearchParameters sParams) throws ApplicationException {
log.debug("Performing RSS Feed Search");
SolrQuery query = null;
if(sParams.getUnformattedQuery().equals("")){
log.debug("Simple Rss Search performed on the String: " + sParams.getQuery());
query = new SolrQuery(sParams.getQuery());
query.set("defType", "dismax");
}else {
log.debug("Simple Rss Search performed on the String: " + sParams.getUnformattedQuery());
query = new SolrQuery(sParams.getUnformattedQuery());
}
query.setTimeAllowed(queryTimeout);
query.setIncludeScore(true); // The relevance (of each results element) to the search terms.
query.setHighlight(false);
// request only fields that we need
query.setFields("id","title_display","publication_date","author_without_collab_display","author_collab_only_display","author_display","volume","issue","article_type","subject_hierarchy","abstract_primary_display","copyright");
query.addFilterQuery("doc_type:full");
query.addFilterQuery("!article_type_facet:\"Issue Image\"");
query.addFacetField("cross_published_journal_key");
// Form field description: "Journals". Query Filter.
if (sParams.getFilterJournals() != null && sParams.getFilterJournals().length > 0) {
query.addFilterQuery(createFilterLimitForJournals(sParams.getFilterJournals()));
}
// Form field description: "Subject Categories". Query Filter.
if (sParams.getFilterSubjects() != null && sParams.getFilterSubjects().length > 0) {
query.addFilterQuery(createFilterLimitForSubject(sParams.getFilterSubjects()));
}
// Form field description: "Article Types". Query Filter.
if (sParams.getFilterArticleType() != null && sParams.getFilterArticleType().length > 0) {
query.addFilterQuery(createFilterLimitForArticleType(sParams.getFilterArticleType()));
}
//Set the sort ordering for results, if applicable.
setSort(query, sParams);
//If the keywords parameter is specified, we need to change what field we're querying against
//aka, body, conclusions, materials and methods ... etc ...
if(sParams.getFilterKeyword().length() > 0) {
String fieldkey = sParams.getFilterKeyword();
if(!validKeywords.containsKey(fieldkey)) {
throw new ApplicationException("Invalid filterKeyword value of " +
fieldkey + " specified");
}
String fieldName = (String)validKeywords.get(fieldkey);
query.set("qf", fieldName);
}
query.set("wt", "xml") ;
Document result = null;
try {
result = solrHttpService.makeSolrRequestForRss(query.toString());
} catch (SolrException e) {
e.printStackTrace();
}
return result;
}
private void setSort(SolrQuery query, FeedSearchParameters sp) throws ApplicationException {
if (log.isDebugEnabled()) {
log.debug("SearchParameters.sort = " + sp.getSort());
}
if (sp.getSort().length() > 0) {
String sortKey = sp.getSort();
String sortValue = (String)validSorts.get(sortKey);
if(sortValue == null) {
throw new ApplicationException("Invalid sort of '" + sp.getSort() + "' specified.");
}
String[] sortOptions = SORT_OPTION_PATTERN.split(sortValue);
for (String sortOption: sortOptions) {
sortOption = sortOption.trim();
int index = sortOption.lastIndexOf(" ");
String fieldName = sortOption;
String sortDirection = null;
if (index != -1) {
fieldName = sortOption.substring(0, index);
sortDirection = sortOption.substring(index + 1).trim();
}
if ( sortDirection == null || ! sortDirection.toLowerCase().equals("asc")) {
query.addSortField(fieldName, SolrQuery.ORDER.desc);
} else {
query.addSortField(fieldName, SolrQuery.ORDER.asc);
}
}
}
if(query.getSortField() == null || query.getSortField().length() == 0) {
//Always default to score if it's not defined
query.addSortField("score", SolrQuery.ORDER.desc);
//If two articles are ranked the same, give the one with a more recent publish date a bump
query.addSortField("publication_date", SolrQuery.ORDER.desc);
//If everything else is equal, order by id
query.addSortField("id", SolrQuery.ORDER.desc);
}
}
private String createFilterLimitForJournals(String[] journals) {
Arrays.sort(journals); // Consistent order so that each filter will only be cached once.
StringBuilder fq = new StringBuilder();
for (String journal : journals) {
fq.append("cross_published_journal_key:").append(journal).append(" OR ");
}
return fq.replace(fq.length() - 4, fq.length(), "").toString(); // Remove last " OR".
}
private String createFilterLimitForSubject(String[] subjects) {
Arrays.sort(subjects); // Consistent order so that each filter will only be cached once.
StringBuilder fq = new StringBuilder();
for (String category : subjects) {
fq.append("subject:\"").append(category).append("\" AND ");
}
return fq.replace(fq.length() - 5, fq.length(), "").toString(); // Remove last " OR".
}
private String createFilterLimitForArticleType(String[] articleTypes) {
Arrays.sort(articleTypes); // Consistent order so that each filter will only be cached once.
StringBuilder fq = new StringBuilder();
for (String articleType : articleTypes) {
fq.append("article_type:\"").append(articleType).append("\" OR ");
}
return fq.replace(fq.length() - 4, fq.length(), "").toString(); // Remove last " OR".
}
private String createFilterFullDocuments() {
return "doc_type:full";
}
@Required
public void setConfiguration(Configuration configuration) throws ApplicationException {
this.configuration = configuration;
StringBuilder hightlightFieldBuilder = new StringBuilder();
queryTimeout = configuration.getInt("ambra.services.search.timeout", 60000); // default to 1 min
if(configuration.containsKey("ambra.services.search.sortOptions.option")) {
validSorts = new HashMap();
displaySorts = new ArrayList();
HierarchicalConfiguration hc = (HierarchicalConfiguration)configuration;
List<HierarchicalConfiguration> sorts =
hc.configurationsAt("ambra.services.search.sortOptions.option");
for (HierarchicalConfiguration s : sorts) {
String key = s.getString("[@displayName]");
String value = s.getString("");
validSorts.put(key, value);
displaySorts.add(key);
}
((HierarchicalConfiguration) configuration).setExpressionEngine(null);
} else {
throw new ApplicationException("ambra.services.search.sortOptions.option not defined " +
"in configuration.");
}
if(configuration.containsKey("ambra.services.search.keywordFields.field")) {
validKeywords = new HashMap();
HierarchicalConfiguration hc = (HierarchicalConfiguration)configuration;
List<HierarchicalConfiguration> sorts =
hc.configurationsAt("ambra.services.search.keywordFields.field");
for (HierarchicalConfiguration s : sorts) {
String key = s.getString("[@displayName]");
String value = s.getString("");
validKeywords.put(key, value);
//These fields can be highlighted too!
if(hightlightFieldBuilder.length() > 0) {
hightlightFieldBuilder.append(",");
}
hightlightFieldBuilder.append(value);
}
} else {
throw new ApplicationException("ambra.services.search.keywordFields.field not defined " +
"in configuration.");
}
}
/**
* @param searchParameters the feedAction data model
* @param journal Current journal
* @return List<String> if article Ids.
* @throws ApplicationException ApplicationException
* @throws java.net.URISyntaxException URISyntaxException
*/
@Override
@Transactional(readOnly = true)
public List<TOCArticle> getIssueArticles(final FeedSearchParameters searchParameters, String journal, String authId) throws
URISyntaxException, ApplicationException {
List<TOCArticle> articleList = new ArrayList<TOCArticle>();
String issurURI = (searchParameters.getIssueURI() != null) ? searchParameters.getIssueURI() : null;
if (issurURI == null) {
Journal curJrnl = journalService.getJournal(journal);
//There is no current issue, return empty result
if (curJrnl.getCurrentIssue() == null) {
return articleList;
}
issurURI = curJrnl.getCurrentIssue().getIssueUri();
}
List<TOCArticleGroup> articleGroups = browseService.getArticleGrpList(issurURI, authId);
for (TOCArticleGroup ag : articleGroups)
articleList.addAll(ag.articles);
return articleList;
}
/**
* Returns a list of annotationViews based on parameters contained in the searchParams. If a start date is not
* specified then a default date is used but not stored in the searchParams.
*
* @param searchParams input parameters.
* @return <code>List<String></code> a list of annotation Ids
* @throws ApplicationException Converts all exceptions to ApplicationException
*/
@Override
@Transactional(readOnly = true)
public List<AnnotationView> getAnnotations(final AnnotationFeedSearchParameters searchParams)
throws ParseException, URISyntaxException {
return annotationService.getAnnotations(
searchParams.getStartDate(), searchParams.getEndDate(), searchParams.getAnnotationTypes(),
searchParams.getMaxResults(), searchParams.getJournal());
}
/**
* Returns a list of trackbackViews based on the parameters. If a start date is not specified then a default date is
* used but not stored in the key.
*
* @param searchParams input params.
* @return <code>List<String></code> a list of annotation Ids
* @throws ApplicationException Converts all exceptions to ApplicationException
*/
@Override
@Transactional(readOnly = true)
public List<LinkbackView> getTrackbacks(final AnnotationFeedSearchParameters searchParams)
throws ParseException, URISyntaxException {
return trackbackService.getTrackbacks(
searchParams.getStartDate(), searchParams.getEndDate(), searchParams.getMaxResults(), searchParams.getJournal());
}
/**
* @param journalService Journal Service
*/
@Required
public void setJournalService(JournalService journalService) {
this.journalService = journalService;
}
/**
* @param annotationService Annotation Service
*/
@Required
public void setAnnotationService(AnnotationService annotationService) {
this.annotationService = annotationService;
}
/**
* @param browseService Browse Service
*/
@Required
public void setBrowseService(BrowseService browseService) {
this.browseService = browseService;
}
/**
* @param trackbackService Trackback Service
*/
@Required
public void setTrackBackService(TrackbackService trackbackService) {
this.trackbackService = trackbackService;
}
/**
* Set solr http service
*
* @param solrHttpService solr http service
*/
@Required
public void setSolrHttpService(SolrHttpService solrHttpService) {
this.solrHttpService = solrHttpService;
}
@Required
public void setSolrFieldConverter(SolrFieldConversion solrFieldConverter) {
this.solrFieldConverter = solrFieldConverter;
}
}