/******************************************************************************* * Copyright (c) 2012, Directors of the Tyndale STEP Project * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * Neither the name of the Tyndale House, Cambridge (www.TyndaleHouse.com) * nor the names of its contributors may be used to endorse or promote * products derived from this software without specific prior written * permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. ******************************************************************************/ package com.tyndalehouse.step.core.service.impl; import static com.tyndalehouse.step.core.utils.ConversionUtils.epochMinutesStringToLocalDateTime; import static com.tyndalehouse.step.core.utils.ConversionUtils.localDateTimeToEpochMinutes; import static com.tyndalehouse.step.core.utils.StringUtils.isBlank; import static org.apache.lucene.search.NumericRangeQuery.newLongRange; import java.util.Arrays; import java.util.Comparator; import javax.inject.Inject; import javax.inject.Singleton; import org.apache.lucene.search.BooleanClause.Occur; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.NumericRangeQuery; import org.joda.time.LocalDateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.tyndalehouse.step.core.data.EntityDoc; import com.tyndalehouse.step.core.data.EntityIndexReader; import com.tyndalehouse.step.core.data.EntityManager; import com.tyndalehouse.step.core.data.entities.aggregations.TimelineEventsAndDate; import com.tyndalehouse.step.core.models.EnhancedTimelineEvent; import com.tyndalehouse.step.core.models.OsisWrapper; import com.tyndalehouse.step.core.service.TimelineService; import com.tyndalehouse.step.core.service.jsword.JSwordPassageService; import com.tyndalehouse.step.core.utils.StringUtils; /** * The implementation of the timeline service, based on JDBC and ORM Lite to access the database. * * @author chrisburrell */ @Singleton public class TimelineServiceImpl implements TimelineService { private static final Logger LOGGER = LoggerFactory.getLogger(TimelineServiceImpl.class); private final JSwordPassageService jsword; private final EntityIndexReader hotspots; private final EntityIndexReader timelineEvents; /** * @param manager the entity manager * @param jsword the jsword service */ @Inject public TimelineServiceImpl(final EntityManager manager, final JSwordPassageService jsword) { this.jsword = jsword; this.hotspots = manager.getReader("hotspot"); this.timelineEvents = manager.getReader("timelineEvent"); } @Override public EntityDoc[] getTimelineConfiguration() { return this.hotspots.search(new MatchAllDocsQuery()); } @Override public TimelineEventsAndDate getEventsFromScripture(final String reference) { final TimelineEventsAndDate timelineEventsAndDate = new TimelineEventsAndDate(); final EntityDoc[] matchingTimelineEvents = lookupEventsMatchingReference(reference); timelineEventsAndDate.setEvents(matchingTimelineEvents); timelineEventsAndDate.setDateTime(getDateForEvents(matchingTimelineEvents)); return timelineEventsAndDate; } /** * Gets the date which is most appropriate for centering around these events, i.e. the median? * * @param matchingTimelineEvents the number of events * @return the localDateTime of the median event, if a duration, then of the start point */ private LocalDateTime getDateForEvents(final EntityDoc[] matchingTimelineEvents) { if (matchingTimelineEvents.length == 0) { return null; } // copy list to new list that can be sorted Arrays.sort(matchingTimelineEvents, new Comparator<EntityDoc>() { @Override public int compare(final EntityDoc o1, final EntityDoc o2) { final String o1StartString = o1.get("fromDate"); final String o2StartString = o2.get("fromDate"); final boolean blankO1 = isBlank(o1StartString); final boolean blankO2 = isBlank(o2StartString); if (blankO1 && blankO2) { return 0; } if (blankO1) { return 1; } if (blankO2) { return -1; } final long o1Start = Long.parseLong(o1StartString); final long o2Start = Long.parseLong(o2StartString); return (o1Start < o2Start) ? -1 : ((o1Start == o2Start) ? 0 : 1); } }); // now we simply return the median element return epochMinutesStringToLocalDateTime(matchingTimelineEvents[matchingTimelineEvents.length / 2] .get("fromDate")); } /** * This method simply takes a reference, resolves it to the kjv versification, and then manages to output * all events that match * * @param reference the reference we are looking for * @return the list of events matching the reference */ @Override public EntityDoc[] lookupEventsMatchingReference(final String reference) { // first get the kjv reference final String allReferences = this.jsword.getAllReferences(reference, "ESV-THE"); if (isBlank(allReferences)) { return new EntityDoc[0]; } // let's assume for now we look up all references LOGGER.debug("Finding events for [{}]", allReferences); return this.timelineEvents.searchSingleColumn("references", allReferences); } @Override public EntityDoc[] getTimelineEvents(final LocalDateTime from, final LocalDateTime to) { final long startMinutes = localDateTimeToEpochMinutes(from); final long endMinutes = localDateTimeToEpochMinutes(to); // start within range final NumericRangeQuery<Long> startInRange = newLongRange("fromDate", startMinutes, endMinutes, true, true); // events that don't have a to date final NumericRangeQuery<Long> haveToDates = newLongRange("toDate", null, null, false, false); // point events should be within range and not have a to date. final BooleanQuery pointEventsInRage = new BooleanQuery(); pointEventsInRage.add(startInRange, Occur.MUST); pointEventsInRage.add(haveToDates, Occur.MUST_NOT); // we want to match those documents that have a from date before the end of the given range, i.e. if // an event finishes 1299BC we want to include it in the range (1300BC, xyz) final NumericRangeQuery<Long> fromIsBeforeEnd = newLongRange("fromDate", null, endMinutes, false, true); // now we also want those that start after the given range final NumericRangeQuery<Long> toIsAfterStart = newLongRange("toDate", startMinutes, null, false, true); // combine the above two queries final BooleanQuery durationsInRange = new BooleanQuery(); durationsInRange.add(fromIsBeforeEnd, Occur.MUST); durationsInRange.add(toIsAfterStart, Occur.MUST); // combine the two queries final BooleanQuery docsInRange = new BooleanQuery(); docsInRange.add(pointEventsInRage, Occur.SHOULD); docsInRange.add(durationsInRange, Occur.SHOULD); return this.timelineEvents.search(docsInRange); } @Override public EnhancedTimelineEvent getTimelineEvent(final String id, final String version) { final EntityDoc[] results = this.timelineEvents.searchExactTermBySingleField("id", 1, id); if (results.length == 0) { return null; } final EnhancedTimelineEvent ete = new EnhancedTimelineEvent(results[0]); final String references = ete.getEvent().get("storedReferences"); final String[] refs = StringUtils.split(references); for (final String r : refs) { // final OsisWrapper osisText = this.jsword.peakOsisText(version, KEYED_REFERENCE_VERSION, r); // ete.add(osisText); } return ete; } }