/* * Copyright 2011 Google Inc. * * 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 com.google.android.apps.iosched.io.gdocs; import com.google.android.apps.iosched.io.gdocs.util.ParserUtils; import com.google.android.apps.iosched.io.gdocs.util.SpreadsheetEntry; import com.google.android.apps.iosched.provider.ScheduleContract; import com.google.android.apps.iosched.provider.ScheduleContract.Sessions; import com.google.android.apps.iosched.provider.ScheduleContract.SyncColumns; import com.google.android.apps.iosched.provider.ScheduleDatabase.SessionsSpeakers; import com.google.android.apps.iosched.provider.ScheduleDatabase.SessionsTracks; import com.google.android.apps.iosched.util.Lists; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; import android.text.format.Time; import android.util.Log; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Locale; import static com.google.android.apps.iosched.io.gdocs.util.ParserUtils.sanitizeId; import static com.google.android.apps.iosched.io.gdocs.util.ParserUtils.splitComma; import static com.google.android.apps.iosched.io.gdocs.util.ParserUtils.translateTrackIdAlias; import static com.google.android.apps.iosched.io.gdocs.util.ParserUtils.AtomTags.ENTRY; import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; import static org.xmlpull.v1.XmlPullParser.START_TAG; /** * Handle a remote {@link XmlPullParser} that defines a set of {@link Sessions} * entries. Assumes that the remote source is a Google Spreadsheet. */ public class RemoteSessionsHandler extends XmlHandler { private static final String TAG = "SessionsHandler"; /** * Custom format used internally that matches expected concatenation of * {@link Columns#SESSION_DATE} and {@link Columns#SESSION_TIME}. */ private static final SimpleDateFormat sTimeFormat = new SimpleDateFormat( "EEEE MMM d yyyy HH:mm Z", Locale.UK); public RemoteSessionsHandler() { super(ScheduleContract.CONTENT_AUTHORITY); } /** {@inheritDoc} */ @Override public ArrayList<ContentProviderOperation> parse(XmlPullParser parser, ContentResolver resolver) throws XmlPullParserException, IOException { final ArrayList<ContentProviderOperation> batch = Lists.newArrayList(); // Walk document, parsing any incoming entries int type; while ((type = parser.next()) != END_DOCUMENT) { if (type == START_TAG && ENTRY.equals(parser.getName())) { // Process single spreadsheet row at a time final SpreadsheetEntry entry = SpreadsheetEntry.fromParser(parser); final String sessionId = sanitizeId(entry.get(Columns.SESSION_TITLE)); final Uri sessionUri = Sessions.buildSessionUri(sessionId); // Check for existing details, only update when changed final ContentValues values = querySessionDetails(sessionUri, resolver); final long localUpdated = values.getAsLong(SyncColumns.UPDATED); final long serverUpdated = entry.getUpdated(); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "found session " + entry.toString()); Log.v(TAG, "found localUpdated=" + localUpdated + ", server=" + serverUpdated); } if (localUpdated >= serverUpdated) continue; final Uri sessionTracksUri = Sessions.buildTracksDirUri(sessionId); final Uri sessionSpeakersUri = Sessions.buildSpeakersDirUri(sessionId); // Clear any existing values for this session, treating the // incoming details as authoritative. batch.add(ContentProviderOperation.newDelete(sessionUri).build()); batch.add(ContentProviderOperation.newDelete(sessionTracksUri).build()); batch.add(ContentProviderOperation.newDelete(sessionSpeakersUri).build()); final ContentProviderOperation.Builder builder = ContentProviderOperation .newInsert(Sessions.CONTENT_URI); builder.withValue(SyncColumns.UPDATED, serverUpdated); builder.withValue(Sessions.SESSION_ID, sessionId); builder.withValue(Sessions.SESSION_LEVEL, entry.get(Columns.SESSION_LEVEL)); builder.withValue(Sessions.SESSION_TITLE, entry.get(Columns.SESSION_TITLE)); builder.withValue(Sessions.SESSION_ABSTRACT, entry.get(Columns.SESSION_ABSTRACT)); builder.withValue(Sessions.SESSION_REQUIREMENTS, entry.get(Columns.SESSION_REQUIREMENTS)); // builder.withValue(Sessions.SESSION_KEYWORDS, entry.get(Columns.SESSION_TAGS)); // builder.withValue(Sessions.SESSION_HASHTAG, entry.get(Columns.SESSION_HASHTAG)); // builder.withValue(Sessions.SESSION_SLUG, entry.get(Columns.SESSION_SLUG)); builder.withValue(Sessions.SESSION_URL, entry.get(Columns.SESSION_URL)); // builder.withValue(Sessions.SESSION_MODERATOR_URL, entry.get(Columns.SESSION_MODERATOR_URL)); builder.withValue(Sessions.SESSION_YOUTUBE_URL, entry.get(Columns.SESSION_YOUTUBE_URL)); builder.withValue(Sessions.SESSION_PDF_URL, entry.get(Columns.SESSION_PDF_URL)); // builder.withValue(Sessions.SESSION_FEEDBACK_URL, entry.get(Columns.SESSION_FEEDBACK_URL)); builder.withValue(Sessions.SESSION_NOTES_URL, entry.get(Columns.SESSION_NOTES_URL)); // Inherit starred value from previous row if (values.containsKey(Sessions.SESSION_STARRED)) { builder.withValue(Sessions.SESSION_STARRED, values.getAsInteger(Sessions.SESSION_STARRED)); } // Parse time string from two columns, which is pretty ugly code // since it assumes the column format is "Wednesday May 19" and // "10:45am to 11:45am". Future spreadsheets should use RFC 3339. final String date = entry.get(Columns.SESSION_DATE); final String time = entry.get(Columns.SESSION_TIME); final int timeSplit = time.indexOf("to"); if (timeSplit == -1) { throw new HandlerException("Expecting " + Columns.SESSION_TIME + " to express span"); } final long startTime = parseTime(date, time.substring(0, timeSplit).trim()); final long endTime = parseTime(date, time.substring(timeSplit + 2).trim()); final String blockId = ParserUtils.findOrCreateBlock( ParserUtils.BLOCK_TITLE_BREAKOUT_SESSIONS, ParserUtils.BLOCK_TYPE_SESSION, startTime, endTime, batch, resolver); builder.withValue(Sessions.BLOCK_ID, blockId); // Assign room final String roomId = sanitizeId(entry.get(Columns.SESSION_ROOM)); builder.withValue(Sessions.ROOM_ID, roomId); // Normal session details ready, write to provider batch.add(builder.build()); // Assign tracks final String[] tracks = splitComma(entry.get(Columns.SESSION_TRACK)); for (String track : tracks) { final String trackId = translateTrackIdAlias(sanitizeId(track)); batch.add(ContentProviderOperation.newInsert(sessionTracksUri) .withValue(SessionsTracks.SESSION_ID, sessionId) .withValue(SessionsTracks.TRACK_ID, trackId).build()); } // Assign speakers final String[] speakers = splitComma(entry.get(Columns.SESSION_SPEAKERS)); for (String speaker : speakers) { final String speakerId = sanitizeId(speaker, true); batch.add(ContentProviderOperation.newInsert(sessionSpeakersUri) .withValue(SessionsSpeakers.SESSION_ID, sessionId) .withValue(SessionsSpeakers.SPEAKER_ID, speakerId).build()); } } } return batch; } /** * Parse the given date and time coming from spreadsheet. This is tightly * tied to a specific format. Ideally, if the source used use RFC 3339 we * could parse quickly using {@link Time#parse3339}. * <p> * Internally assumes PST time zone and year 2011. * * @param date String of format "Wednesday May 19", usually read from * {@link Columns#SESSION_DATE}. * @param time String of format "10:45am", usually after splitting * {@link Columns#SESSION_TIME}. */ private static long parseTime(String date, String time) throws HandlerException { final String composed = String.format("%s 2012 %s +0100", date, time); try { return sTimeFormat.parse(composed).getTime(); } catch (java.text.ParseException e) { throw new HandlerException("Problem parsing timestamp", e); } } private static ContentValues querySessionDetails(Uri uri, ContentResolver resolver) { final ContentValues values = new ContentValues(); final Cursor cursor = resolver.query(uri, SessionsQuery.PROJECTION, null, null, null); try { if (cursor.moveToFirst()) { values.put(SyncColumns.UPDATED, cursor.getLong(SessionsQuery.UPDATED)); values.put(Sessions.SESSION_STARRED, cursor.getInt(SessionsQuery.STARRED)); } else { values.put(SyncColumns.UPDATED, ScheduleContract.UPDATED_NEVER); } } finally { cursor.close(); } return values; } private interface SessionsQuery { String[] PROJECTION = { SyncColumns.UPDATED, Sessions.SESSION_STARRED, }; int UPDATED = 0; int STARRED = 1; } /** Columns coming from remote spreadsheet. */ private interface Columns { String SESSION_DATE = "sessiondate"; String SESSION_TIME = "sessiontime"; String SESSION_ROOM = "sessionroom"; String SESSION_TRACK = "sessiontrack"; String SESSION_LEVEL = "sessionlevel"; String SESSION_TITLE = "sessiontitle"; String SESSION_TAGS = "sessiontags"; String SESSION_HASHTAG = "sessionhashtag"; String SESSION_SLUG = "sessionslug"; String SESSION_SPEAKERS = "sessionspeakers"; String SESSION_ABSTRACT = "sessionabstract"; String SESSION_REQUIREMENTS = "sessionrequirements"; String SESSION_URL = "sessionurl"; String SESSION_MODERATOR_URL = "sessionmoderatorurl"; String SESSION_YOUTUBE_URL = "sessionyoutubeurl"; String SESSION_PDF_URL = "sessionpdfurl"; String SESSION_FEEDBACK_URL = "sessionfeedbackurl"; String SESSION_NOTES_URL = "sessionnotesurl"; // session_date: Wednesday May 19 // session_time: 10:45am-11:45am // session_room: 6 // session_track: Enterprise, App Engine // session_level: 201 // session_title: Run corporate applications on Google App Engine? Yes we do. // session_slug: run-corporate-applications // session_tags: Enterprise, SaaS, PaaS, Hosting, App Engine, Java // session_speakers: Ben Fried, John Smith // session_abstract: And you can too! Come hear Google's CIO Ben Fried describe... // session_requirements: None // session_url: http://www.google.com/events/io/2011/foo // session_hashtag: #io11android1 // session_youtube_url // session_pdf_url // session_feedback_url // session_moderator_url // session_notes_url } }