package org.fluxtream.core.services.impl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TimeZone;
import java.util.TreeSet;
import org.fluxtream.core.OutsideTimeBoundariesException;
import org.fluxtream.core.SimpleTimeInterval;
import org.fluxtream.core.TimeInterval;
import org.fluxtream.core.TimeUnit;
import org.fluxtream.core.aspects.FlxLogger;
import org.fluxtream.core.connectors.Connector;
import org.fluxtream.core.connectors.ObjectType;
import org.fluxtream.core.connectors.annotations.ObjectTypeSpec;
import org.fluxtream.core.connectors.vos.AbstractFacetVO;
import org.fluxtream.core.connectors.vos.AbstractInstantFacetVO;
import org.fluxtream.core.connectors.vos.AbstractPhotoFacetVO;
import org.fluxtream.core.domain.AbstractFacet;
import org.fluxtream.core.domain.ApiKey;
import org.fluxtream.core.domain.CoachingBuddy;
import org.fluxtream.core.domain.GuestSettings;
import org.fluxtream.core.domain.PhotoFacetFinderStrategy;
import org.fluxtream.core.domain.TagFilter;
import org.fluxtream.core.services.GuestService;
import org.fluxtream.core.services.PhotoService;
import org.fluxtream.core.services.SettingsService;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
/**
* @author Chris Bartley (bartley@cmu.edu)
*/
@Service
@Component
public class PhotoServiceImpl implements PhotoService {
private static final FlxLogger LOG = FlxLogger.getLogger("Fluxtream");
private static final FlxLogger LOG_DEBUG = FlxLogger.getLogger("Fluxtream");
@Autowired
SettingsService settingsService;
@Autowired
GuestService guestService;
@Autowired
BeanFactory beanFactory;
private static abstract class PhotoFinder {
final Map<ObjectType, List<AbstractFacet>> find(final ApiKey apiKey, @Nullable TagFilter tagFilter) {
final Map<ObjectType, List<AbstractFacet>> facets = new HashMap<ObjectType, List<AbstractFacet>>();
if (apiKey.getConnector() != null) {
final ObjectType[] objectTypes = apiKey.getConnector().objectTypes();
if (objectTypes != null) {
for (final ObjectType objectType : objectTypes) {
if (objectType.isImageType()) {
facets.put(objectType, find(apiKey, objectType, tagFilter));
}
}
}
}
return facets;
}
protected abstract List<AbstractFacet> find(final ApiKey apiKey,
final ObjectType objectType,
@Nullable TagFilter tagFilter);
}
@Override
public SortedSet<Photo> getPhotos(long guestId,
TimeInterval timeInterval) throws ClassNotFoundException, IllegalAccessException, InstantiationException, OutsideTimeBoundariesException {
return getPhotos(guestId, timeInterval, ALL_DEVICES_NAME, DEFAULT_PHOTOS_CHANNEL_NAME, null, null);
}
@Override
public SortedSet<Photo> getPhotos(final long guestId,
final TimeInterval timeInterval,
final String connectorPrettyName,
final String objectTypeName,
@Nullable TagFilter tagFilter) throws ClassNotFoundException, IllegalAccessException, InstantiationException, OutsideTimeBoundariesException {
return getPhotos(guestId, timeInterval, connectorPrettyName, objectTypeName, tagFilter, new PhotoFinder() {
public List<AbstractFacet> find(final ApiKey apiKey,
final ObjectType objectType,
@Nullable TagFilter tagFilter) {
LOG_DEBUG.debug("PhotoServiceImpl.find(): finding photos for ApiKey [" + apiKey + "] and ObjectType [" + objectType + "] having TagFilter [" + tagFilter + "]");
final PhotoFacetFinderStrategy photoFacetFinderStrategy = getPhotoFacetFinderStrategyFromObjectType(objectType);
if (photoFacetFinderStrategy != null) {
return photoFacetFinderStrategy.findAll(apiKey, objectType, timeInterval, tagFilter);
}
return new ArrayList<AbstractFacet>(0);
}
});
}
@Override
public SortedSet<Photo> getPhotos(final long guestId,
final long timeInMillis,
final String connectorPrettyName,
final String objectTypeName,
final int desiredCount,
final boolean isGetPhotosBeforeTime,
@Nullable TagFilter tagFilter) throws InstantiationException, IllegalAccessException, ClassNotFoundException, OutsideTimeBoundariesException {
// make sure the count is >= 1
final int cleanedDesiredCount = Math.max(1, desiredCount);
final SortedSet<Photo> photos = getPhotos(guestId, null, connectorPrettyName, objectTypeName, tagFilter, new PhotoFinder() {
public List<AbstractFacet> find(final ApiKey apiKey,
final ObjectType objectType,
@Nullable TagFilter tagFilter) {
LOG_DEBUG.debug("PhotoServiceImpl.find(): finding photos for ApiKey [" + apiKey + "] and ObjectType [" + objectType + "] having TagFilter [" + tagFilter + "]");
final PhotoFacetFinderStrategy photoFacetFinderStrategy = getPhotoFacetFinderStrategyFromObjectType(objectType);
if (photoFacetFinderStrategy != null) {
if (isGetPhotosBeforeTime) {
return photoFacetFinderStrategy.findBefore(apiKey, objectType, timeInMillis, cleanedDesiredCount, tagFilter);
}
else {
return photoFacetFinderStrategy.findAfter(apiKey, objectType, timeInMillis, cleanedDesiredCount, tagFilter);
}
}
return new ArrayList<AbstractFacet>(0);
}
});
// Make sure we don't return more than requested (which may happen if we're merging from multiple photo channels,
// which can happen for the All.photos device/channel).
if (photos.size() > cleanedDesiredCount) {
// first convert to a list for easier extraction
List<Photo> photosList = new ArrayList<Photo>(photos);
final SortedSet<Photo> photosSubset = new TreeSet<Photo>();
if (isGetPhotosBeforeTime) {
// get the last N photos
photosSubset.addAll(photosList.subList((photosList.size() - cleanedDesiredCount), photosList.size()));
}
else {
// get the first N photos
photosSubset.addAll(photosList.subList(0, cleanedDesiredCount));
}
return photosSubset;
}
return photos;
}
@Override
public Map<String, TimeInterval> getPhotoChannelTimeRanges(final long guestId, final CoachingBuddy coachee) {
// TODO: This could really benefit from some caching. The time ranges can only change upon updating a photo
// connector so it would be better to cache this info and then just refresh it whenever the connector is updated
Map<String, TimeInterval> photoChannelTimeRanges = new HashMap<String, TimeInterval>();
List<ApiKey> userKeys = guestService.getApiKeys(guestId);
for (ApiKey apiKey : userKeys) {
Connector connector = null;
if (apiKey != null) {
connector = apiKey.getConnector();
}
if (connector != null && connector.getName() != null &&
(coachee == null || coachee.hasAccessToConnector(connector.getName())) && connector.hasImageObjectType()) {
// Check the object types, if any, to find the image object type(s)
ObjectType[] objectTypes = apiKey.getConnector().objectTypes();
if (objectTypes == null) {
final String channelName = constructChannelName(connector, null);
final TimeInterval timeInterval = constructTimeIntervalFromOldestAndNewestFacets(apiKey, null);
photoChannelTimeRanges.put(channelName, timeInterval);
}
else {
for (ObjectType objectType : objectTypes) {
if (objectType.isImageType()) {
final String channelName = constructChannelName(connector, objectType);
final TimeInterval timeInterval = constructTimeIntervalFromOldestAndNewestFacets(apiKey, objectType);
photoChannelTimeRanges.put(channelName, timeInterval);
}
}
}
}
}
return photoChannelTimeRanges;
}
private PhotoFacetFinderStrategy getPhotoFacetFinderStrategyFromObjectType(final ObjectType objectType) {
if (objectType != null) {
try {
final Class<? extends AbstractFacet> facetClass = objectType.facetClass();
final ObjectTypeSpec objectTypeSpec = facetClass.getAnnotation(ObjectTypeSpec.class);
final Class<? extends PhotoFacetFinderStrategy> photoFacetFinderStrategyClass = objectTypeSpec.photoFacetFinderStrategy();
return beanFactory.getBean(photoFacetFinderStrategyClass);
}
catch (Exception e) {
LOG.error("Exception caught while trying trying to instantiate the PhotoFacetFinderStrategy from objectType [" + objectType + "]. Returning null.", e);
}
}
return null;
}
/**
* Returns all photos for {@link Connector}(s) specified by the given <code>connectorPrettyName</code>, and
* optionally narrowed by the {@link ObjectType} specified by the given <code>objectTypeName</code>. If the
* <code>connectorPrettyName</code> is equal to the {@link #ALL_DEVICES_NAME}, then this method checks every
* Connector for whether it has an image ObjectType and, if so, adds the relevant photos from each image ObjectType
* belonging to the Connector. If the <code>connectorPrettyName</code> is not equal to the
* {@link #ALL_DEVICES_NAME}, then this method finds the specified Connector and ObjectType and adds the photos.
* Furthermore, if the objectTypeName does not specify an existing ObjectType for the Connector, then this method
* returns photos from from all ObjectTypes which are of {@link ObjectType#isImageType() image type}. The set of
* returned photos may also be optionally filtered by the given <code>tags</code> and <code>tagFilteringStrategy</code>.
*
* May return an empty {@link SortedSet}, but guaranteeed to not return <code>null</code>.
*/
private SortedSet<Photo> getPhotos(final long guestId,
final TimeInterval timeInterval,
final String connectorPrettyName,
final String objectTypeName,
@Nullable TagFilter tagFilter,
final PhotoFinder facetFinderStrategy)
throws InstantiationException, IllegalAccessException, ClassNotFoundException, OutsideTimeBoundariesException
{
SortedSet<Photo> photos = new TreeSet<Photo>();
if (ALL_DEVICES_NAME.equals(connectorPrettyName)) {
List<ApiKey> userKeys = guestService.getApiKeys(guestId);
for (ApiKey apiKey : userKeys) {
Connector connector = null;
if (apiKey != null && apiKey.getConnector() != null) {
connector = apiKey.getConnector();
}
if (connector != null && connector.hasImageObjectType()) {
final ObjectType[] objectTypes = connector.objectTypes();
if (objectTypes != null) {
for (ObjectType objectType : objectTypes) {
if (objectType.isImageType()) {
List<AbstractFacet> facets = facetFinderStrategy.find(apiKey, objectType, tagFilter);
photos.addAll(convertFacetsToPhotos(apiKey, timeInterval, facets, connector, objectType));
}
}
}
}
}
}
else {
final ApiKey apiKey = findConnectorApiKeyByPrettyName(guestId, connectorPrettyName);
if (apiKey != null && apiKey.getConnector() != null) {
final Connector connector = apiKey.getConnector();
final ObjectType desiredObjectType = findObjectTypeByName(connector, objectTypeName);
if (desiredObjectType == null) {
final Map<ObjectType, List<AbstractFacet>> facetsByObjectType = facetFinderStrategy.find(apiKey, tagFilter);
if ((facetsByObjectType != null) && (!facetsByObjectType.isEmpty())) {
for (final ObjectType objectType : facetsByObjectType.keySet()) {
final List<AbstractFacet> facets = facetsByObjectType.get(objectType);
if (facets != null) {
photos.addAll(convertFacetsToPhotos(apiKey, timeInterval, facets, connector, objectType));
}
}
}
}
else if (desiredObjectType.isImageType()) {
final List<AbstractFacet> facets = facetFinderStrategy.find(apiKey, desiredObjectType, tagFilter);
if (facets != null) {
photos.addAll(convertFacetsToPhotos(apiKey, timeInterval, facets, connector, desiredObjectType));
}
}
}
}
return photos;
}
/** Returns the Connector having the given pretty name. Returns <code>null</code> if no such connector exists. */
private ApiKey findConnectorApiKeyByPrettyName(final long guestId, final String connectorPrettyName) {
List<ApiKey> userKeys = guestService.getApiKeys(guestId);
for (ApiKey key : userKeys) {
if (key != null) {
final Connector connector = key.getConnector();
if (connector != null && ((connector.prettyName() != null && connector.prettyName().equals(connectorPrettyName)) || (connector.getName().equals(connectorPrettyName)))) {
return key;
}
}
}
return null;
}
/**
* Returns the ObjectType for the given Connector having the given name. If no such ObjectType exists, or
* the connector doesn't have any ObjectTypes, or then this method returns <code>null</code>.
*/
private ObjectType findObjectTypeByName(final Connector connector, final String objectTypeName) {
if (connector != null && objectTypeName != null) {
ObjectType[] objectTypes = connector.objectTypes();
if (objectTypes != null) {
for (final ObjectType objectType : objectTypes) {
if (objectTypeName.equals(objectType.getName())) {
return objectType;
}
}
}
}
return null;
}
/**
* Converts {@link AbstractFacet}s to {@link Photo}s. If the given {@link SimpleTimeInterval} is <code>null</code>, this
* method creates a new one for each {@link AbstractFacet} using the facet's start time.
*/
private SortedSet<Photo> convertFacetsToPhotos(final ApiKey apiKey,
final TimeInterval timeInterval,
final List<AbstractFacet> facets,
final Connector connector,
final ObjectType objectType)
throws ClassNotFoundException, InstantiationException, IllegalAccessException, OutsideTimeBoundariesException
{
SortedSet<Photo> photos = new TreeSet<Photo>();
GuestSettings settings = settingsService.getSettings(apiKey.getGuestId());
for (AbstractFacet facet : facets) {
Class<? extends AbstractFacetVO<AbstractFacet>> jsonFacetClass = AbstractFacetVO.getFacetVOClass(facet);
AbstractInstantFacetVO<AbstractFacet> facetVo = (AbstractInstantFacetVO<AbstractFacet>)jsonFacetClass.newInstance();
final TimeInterval actualTimeInterval;
if (timeInterval == null) {
actualTimeInterval = new SimpleTimeInterval(facet.start, facet.start, TimeUnit.ARBITRARY, TimeZone.getTimeZone("UTC"));
}
else {
actualTimeInterval = timeInterval;
}
facetVo.extractValues(facet, actualTimeInterval, settings);
photos.add(new PhotoImpl((AbstractPhotoFacetVO)facetVo, connector, objectType));
}
return photos;
}
/**
* Constructs a channel name as the concatenation of the Connector pretty name and the ObjectType name. Uses
* the {@link #DEFAULT_PHOTOS_CHANNEL_NAME} if the ObjectType is <code>null</code>.
*/
private String constructChannelName(final Connector connector, final ObjectType objectType) {
return connector.prettyName() + "." + (objectType == null ? DEFAULT_PHOTOS_CHANNEL_NAME : objectType.getName());
}
/**
* Returns the {@link SimpleTimeInterval} for the oldest and newest facets. Returns <code>null</code> if no facets exist.
*/
private TimeInterval constructTimeIntervalFromOldestAndNewestFacets(final ApiKey apiKey,
final ObjectType objectType) {
final PhotoFacetFinderStrategy photoFacetFinderStrategy = getPhotoFacetFinderStrategyFromObjectType(objectType);
final AbstractFacet oldestFacet = photoFacetFinderStrategy.findOldest(apiKey, objectType);
final AbstractFacet newestFacet = photoFacetFinderStrategy.findLatest(apiKey, objectType);
if (oldestFacet != null && newestFacet != null) {
return new SimpleTimeInterval(oldestFacet.start, newestFacet.start, TimeUnit.ARBITRARY, TimeZone.getTimeZone("UTC"));
}
return null;
}
private static final class PhotoImpl implements Photo, Comparable<Photo> {
private final AbstractPhotoFacetVO facetVo;
private final Connector connector;
private final ObjectType objectType;
private PhotoImpl(final AbstractPhotoFacetVO facetVo, final Connector connector, final ObjectType objectType) {
this.facetVo = facetVo;
this.connector = connector;
this.objectType = objectType;
}
@Override
public AbstractPhotoFacetVO getAbstractPhotoFacetVO() {
return facetVo;
}
@Override
public Connector getConnector() {
return connector;
}
@Override
public ObjectType getObjectType() {
return objectType;
}
@Override
public int compareTo(final Photo that) {
final Long thisStart = this.getAbstractPhotoFacetVO().start;
final Long thatStart = that.getAbstractPhotoFacetVO().start;
int comparison = thisStart.compareTo(thatStart);
if (comparison != 0) {
return comparison;
}
final String thisId = this.getConnector().getName() + "." + this.getObjectType().getName() + "." + this.getAbstractPhotoFacetVO().id + "." + this.getAbstractPhotoFacetVO().getPhotoUrl();
final String thatId = that.getConnector().getName() + "." + that.getObjectType().getName() + "." + that.getAbstractPhotoFacetVO().id + "." + that.getAbstractPhotoFacetVO().getPhotoUrl();
return thisId.compareTo(thatId);
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final PhotoImpl photo = (PhotoImpl)o;
if (connector != null ? !connector.equals(photo.connector) : photo.connector != null) {
return false;
}
if (facetVo != null ? !facetVo.equals(photo.facetVo) : photo.facetVo != null) {
return false;
}
if (objectType != null ? !objectType.equals(photo.objectType) : photo.objectType != null) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = facetVo != null ? facetVo.hashCode() : 0;
result = 31 * result + (connector != null ? connector.hashCode() : 0);
result = 31 * result + (objectType != null ? objectType.hashCode() : 0);
return result;
}
}
}