package org.fluxtream.connectors.flickr; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.SortedSet; import java.util.TreeSet; import net.sf.json.JSONArray; import net.sf.json.JSONObject; import org.fluxtream.core.connectors.ObjectType; import org.fluxtream.core.connectors.annotations.Updater; import org.fluxtream.core.connectors.location.LocationFacet; import org.fluxtream.core.connectors.updaters.AbstractUpdater; import org.fluxtream.core.connectors.updaters.AuthExpiredException; import org.fluxtream.core.connectors.updaters.UpdateFailedException; import org.fluxtream.core.connectors.updaters.UpdateInfo; import org.fluxtream.core.domain.ApiKey; import org.fluxtream.core.domain.ChannelMapping; import org.fluxtream.core.domain.Tag; import org.fluxtream.core.services.ApiDataService; import org.fluxtream.core.services.GuestService; import org.fluxtream.core.services.JPADaoService; import org.fluxtream.core.services.MetadataService; import org.fluxtream.core.services.impl.BodyTrackHelper; import org.fluxtream.core.utils.JPAUtils; import org.fluxtream.core.utils.UnexpectedHttpResponseCodeException; import org.fluxtream.core.utils.Utils; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import static org.fluxtream.core.utils.HttpUtils.fetch; import static org.fluxtream.core.utils.Utils.hash; /** * @author candide * */ @Component @Updater(prettyName = "Flickr", value = 11, objectTypes = FlickrPhotoFacet.class, defaultChannels = {"Flickr.photo"}) public class FlickrUpdater extends AbstractUpdater { @Autowired BodyTrackHelper bodyTrackHelper; @Autowired GuestService guestService; private static final DateTimeFormatter format = DateTimeFormat .forPattern("yyyy-MM-dd HH:mm:ss").withZone(DateTimeZone.UTC); private static final int ITEMS_PER_PAGE = 500; @Autowired JPADaoService jpaDaoService; @Autowired MetadataService metadataService; public FlickrUpdater() { super(); } String sign(ApiKey apiKey, Map<String, String> parameters) throws NoSuchAlgorithmException { String toSign = guestService.getApiKeyAttribute(apiKey, "flickrConsumerSecret"); SortedSet<String> eachKey = new TreeSet<String>(parameters.keySet()); for (String key : eachKey) toSign += key + parameters.get(key); String sig = hash(toSign); return sig; } @Override protected void updateConnectorDataHistory(UpdateInfo updateInfo) throws Exception { // taking care of resetting the data if things went wrong before //if (!connectorUpdateService.isHistoryUpdateCompleted( updateInfo.apiKey, -1)) // apiDataService.eraseApiData(updateInfo.apiKey, -1); int page = 0, pages; do { page++; JSONObject feed = retrievePhotoHistory(updateInfo, 0, System.currentTimeMillis(), page); if (feed.has("stat")) { String stat = feed.getString("stat"); if (stat.equalsIgnoreCase("fail")) { String message = feed.getString("message"); throw new RuntimeException("Could not retrieve Flickr history: " + message); } } JSONObject photosWrapper = feed.getJSONObject("photos"); if (photosWrapper != null) { pages = photosWrapper.getInt("pages"); page = photosWrapper.getInt("page"); JSONArray photos = photosWrapper.getJSONArray("photo"); createOrUpdatePhotos(photos, updateInfo); } else break; } while (page<pages); } @Override public void updateConnectorData(UpdateInfo updateInfo) throws Exception { Long lastUpdatedTime = getLastUpdatedTime(updateInfo); // taking care of resetting the data if we are coming from a database that // hasn't tracked the updatedate yet if (lastUpdatedTime==null||lastUpdatedTime==0) { apiDataService.eraseApiData(updateInfo.apiKey, -1); updateConnectorDataHistory(updateInfo); return; } int page = 0, pages; do { page++; JSONObject feed = retrieveRecentlyUpdatedPhotos(updateInfo, lastUpdatedTime, page); if (feed.has("stat")) { String stat = feed.getString("stat"); if (stat.equalsIgnoreCase("fail")) { String message = feed.getString("message"); if (message.indexOf("Invalid auth token")!=-1) { throw new AuthExpiredException(); } else throw new UpdateFailedException("Could not retrieve flickr recently updated photos: " + message, true, ApiKey.PermanentFailReason.unknownReason(message)); } } JSONObject photosWrapper = feed.getJSONObject("photos"); pages = photosWrapper.getInt("pages"); page = photosWrapper.getInt("page"); if (photosWrapper != null) { JSONArray photos = photosWrapper.getJSONArray("photo"); createOrUpdatePhotos(photos, updateInfo); } else break; } while (page<pages); } private void createOrUpdatePhotos(final JSONArray photos, final UpdateInfo updateInfo) throws Exception { final List<LocationFacet> locationResources = new ArrayList<LocationFacet>(); for (int i=0; i<photos.size(); i++) { final JSONObject photo = photos.getJSONObject(i); String flickrId = photo.getString("id"); final ApiDataService.FacetQuery facetQuery = new ApiDataService.FacetQuery("e.apiKeyId=? AND e.flickrId=?", updateInfo.apiKey.getId(), flickrId); final ApiDataService.FacetModifier<FlickrPhotoFacet> facetModifier = new ApiDataService.FacetModifier<FlickrPhotoFacet>() { @Override public FlickrPhotoFacet createOrModify(FlickrPhotoFacet origFacet, final Long apiKeyId) { FlickrPhotoFacet facet=origFacet; try { if (facet==null) { facet = new FlickrPhotoFacet(updateInfo.apiKey.getId()); facet.flickrId = photo.getString("id"); facet.api = updateInfo.apiKey.getConnector().value(); facet.guestId = updateInfo.apiKey.getGuestId(); facet.timeUpdated = System.currentTimeMillis(); } facet.owner = photo.getString("owner"); facet.secret = photo.getString("secret"); facet.server = photo.getString("server"); facet.farm = photo.getString("farm"); facet.title = photo.getString("title"); final JSONObject descriptionObject = photo.getJSONObject("description"); if (descriptionObject != null) { facet.comment = descriptionObject.getString("_content"); } facet.ispublic = Integer.valueOf(photo.getString("ispublic")) == 1; facet.isfriend = Integer.valueOf(photo.getString("isfriend")) == 1; facet.isfamily = Integer.valueOf(photo.getString("isfamily")) == 1; final String datetaken = photo.getString("datetaken"); final DateTime dateTime = format.parseDateTime(datetaken); facet.startTimeStorage = facet.endTimeStorage = toTimeStorage(dateTime.getYear(), dateTime.getMonthOfYear(), dateTime.getDayOfMonth(), dateTime.getHourOfDay(), dateTime.getMinuteOfHour(), 0); facet.date = (new StringBuilder(String.valueOf(dateTime.getYear())).append("-") .append(pad(dateTime.getMonthOfYear())).append("-") .append(pad(dateTime.getDayOfMonth()))).toString(); facet.datetaken = dateTime.getMillis(); facet.start = dateTime.getMillis(); facet.end = dateTime.getMillis(); facet.dateupload = photo.getLong("dateupload")*1000; if (photo.has("lastupdate")) facet.dateupdated = photo.getLong("lastupdate")*1000; facet.accuracy = photo.getInt("accuracy"); facet.addTags(photo.getString("tags"), Tag.SPACE_DELIMITER); if (photo.getString("latitude")!=null && photo.getString("longitude")!=null) { final Float latitude = Float.valueOf(photo.getString("latitude")); final Float longitude = Float.valueOf(photo.getString("longitude")); if (latitude!=0 && longitude!=0) { facet.latitude = latitude; facet.longitude = longitude; addLocation(updateInfo, locationResources, facet, dateTime); } } return facet; } catch (Throwable e) { // Attempt to parse this photo failed. Return the original facet. // If it was null then nothing is persisted. If it was not null then // whatever changes we made before we died will be persisted, which is // really the best we can do // TODO: generate notification of failed import using getPhotoUrl return(origFacet); } } }; // we could use the resulting value (facet) from this call if we needed to do further processing on it (e.g. passing it on to the datastore) apiDataService.createOrReadModifyWrite(FlickrPhotoFacet.class, facetQuery, facetModifier, updateInfo.apiKey.getId()); } if (locationResources.size()>0) metadataService.updateLocationMetadata(updateInfo.getGuestId(), locationResources); } public String getPhotoUrl(final JSONObject photoJson) { // TODO: Return a string of the form http://www.flickr.com/photos/<owner>/<id>/edit-details/ // photo.getString("id"); // photo.getString("owner"); return null; } private Long getLastUpdatedTime(final UpdateInfo updateInfo) { final String entityName = JPAUtils.getEntityName(FlickrPhotoFacet.class); final List<FlickrPhotoFacet> facets = jpaDaoService.executeQueryWithLimit("SELECT facet from " + entityName + " facet WHERE facet.apiKeyId=? ORDER BY facet.dateupdated DESC", 1, FlickrPhotoFacet.class, updateInfo.apiKey.getId()); if (facets.size()==0) return new Long(0); final Long dateupdated = facets.get(0).dateupdated; if (dateupdated!=null) { return dateupdated + 1000; } else return null; } private JSONObject retrievePhotoHistory(UpdateInfo updateInfo, long from, long to, int page) throws Exception { long then = System.currentTimeMillis(); // The start/end upload dates should be in the form of a unix timestamp (see http://www.flickr.com/services/api/flickr.people.getPhotos.htm) String startDate = String.valueOf(from / 1000); String endDate = String.valueOf(to / 1000); final Map<String, String> otherParams = new HashMap<String, String>(); otherParams.put("method", "flickr.people.getPhotos"); otherParams.put("min_upload_date", startDate); otherParams.put("max_upload_date", endDate); String searchPhotosUrl = buildFlickrAPIUrl(updateInfo, page, otherParams); String photosJson; try { photosJson = fetch(searchPhotosUrl); countSuccessfulApiCall(updateInfo.apiKey, updateInfo.objectTypes, then, searchPhotosUrl); } catch (UnexpectedHttpResponseCodeException e) { countFailedApiCall(updateInfo.apiKey, updateInfo.objectTypes, then, searchPhotosUrl, Utils.stackTrace(e), e.getHttpResponseCode(), e.getHttpResponseMessage()); if (e.getHttpResponseCode()>=400 && e.getHttpResponseCode()<500) throw new UpdateFailedException("Unexpected response code: " + e.getHttpResponseCode(), new Exception(), true, ApiKey.PermanentFailReason. clientError(e.getHttpResponseCode(), e.getHttpResponseMessage())); throw new UpdateFailedException(e, false, null); } catch (IOException e) { reportFailedApiCall(updateInfo.apiKey, updateInfo.objectTypes, then, searchPhotosUrl, Utils.stackTrace(e), "I/O"); throw e; } if (photosJson == null || photosJson.equals("")) throw new Exception( "empty json string returned from flickr API call"); JSONObject feed = JSONObject.fromObject(photosJson); return feed; } private JSONObject retrieveRecentlyUpdatedPhotos(UpdateInfo updateInfo, long lastUpdate, int page) throws Exception { long then = System.currentTimeMillis(); // The start/end upload dates should be in the form of a unix timestamp (see http://www.flickr.com/services/api/flickr.people.getPhotos.htm) String lastupdate= String.valueOf(lastUpdate / 1000); final Map<String, String> otherParams = new HashMap<String, String>(); otherParams.put("method", "flickr.photos.recentlyUpdated"); otherParams.put("min_date", lastupdate); String searchPhotosUrl = buildFlickrAPIUrl(updateInfo, page, otherParams); String photosJson; try { photosJson = fetch(searchPhotosUrl); countSuccessfulApiCall(updateInfo.apiKey, updateInfo.objectTypes, then, searchPhotosUrl); } catch (UnexpectedHttpResponseCodeException e) { countFailedApiCall(updateInfo.apiKey, updateInfo.objectTypes, then, searchPhotosUrl, Utils.stackTrace(e), e.getHttpResponseCode(), e.getHttpResponseMessage()); if (e.getHttpResponseCode()>=400 && e.getHttpResponseCode()<500) throw new UpdateFailedException("Unexpected response code: " + e.getHttpResponseCode(), new Exception(), true, ApiKey.PermanentFailReason.clientError(e.getHttpResponseCode(), e.getHttpResponseMessage())); throw new UpdateFailedException(e, false, null); } catch (IOException e) { reportFailedApiCall(updateInfo.apiKey, updateInfo.objectTypes, then, searchPhotosUrl, Utils.stackTrace(e), "I/O"); throw e; } if (photosJson == null || photosJson.equals("")) throw new Exception( "empty json string returned from flickr API call"); JSONObject feed = JSONObject.fromObject(photosJson); return feed; } private String buildFlickrAPIUrl(final UpdateInfo updateInfo, final int page, final Map<String, String> otherParams) throws NoSuchAlgorithmException { final String api_key = guestService.getApiKeyAttribute(updateInfo.apiKey, "flickrConsumerKey"); final String nsid = guestService.getApiKeyAttribute( updateInfo.apiKey, "nsid"); final String token = guestService.getApiKeyAttribute(updateInfo.apiKey, "token"); final Map<String, String> params = new HashMap<String, String>(); params.put("api_key", api_key); params.put("user_id", nsid); params.put("auth_token", token); params.put("per_page", String.valueOf(ITEMS_PER_PAGE)); params.put("page", String.valueOf(page)); params.put("format", "json"); params.put("nojsoncallback", "1"); params.put("extras", "date_upload,date_taken,description,geo,tags,last_update"); params.putAll(otherParams); final String api_sig = sign(updateInfo.apiKey, params); final StringBuilder urlBuilder = new StringBuilder("https://api.flickr.com/services/rest/?"); for (Map.Entry<String, String> parameter : params.entrySet()) urlBuilder.append(parameter.getKey()).append("=").append(parameter.getValue()).append("&"); urlBuilder.append("api_sig=").append(api_sig); String searchPhotosUrl = urlBuilder.toString(); searchPhotosUrl = searchPhotosUrl.replace(" ", "%20"); return searchPhotosUrl; } private static String pad(int i) { return i<10 ? (new StringBuilder("0").append(i)).toString() : String.valueOf(i); } private String toTimeStorage(int year, int month, int day, int hours, int minutes, int seconds) { //yyyy-MM-dd'T'HH:mm:ss.SSS return (new StringBuilder()).append(year) .append("-").append(pad(month)).append("-") .append(pad(day)).append("T").append(pad(hours)) .append(":").append(pad(minutes)).append(":") .append(pad(seconds)).append(".000").toString(); } private void addLocation(final UpdateInfo updateInfo, final List<LocationFacet> locationResources, final FlickrPhotoFacet facet, final DateTime dateTime) { LocationFacet locationResource = new LocationFacet(); locationResource.guestId = facet.guestId; locationResource.latitude = facet.latitude; locationResource.longitude = facet.longitude; locationResource.source = LocationFacet.Source.FLICKR; locationResource.timestampMs = dateTime.getMillis(); locationResource.apiKeyId = updateInfo.apiKey.getId(); locationResource.start = dateTime.getMillis(); locationResource.end = dateTime.getMillis(); locationResource.api = updateInfo.apiKey.getConnector().value(); locationResources.add(locationResource); } @Override public void setDefaultChannelStyles(ApiKey apiKey) {} }