package org.dodgybits.shuffle.android.synchronisation.tracks; import java.util.Collection; import java.util.HashMap; import java.util.Map; import org.apache.http.HttpStatus; import org.apache.http.StatusLine; import org.dodgybits.shuffle.android.core.model.EntityBuilder; import org.dodgybits.shuffle.android.core.model.Id; import org.dodgybits.shuffle.android.core.model.persistence.EntityPersister; import org.dodgybits.shuffle.android.persistence.provider.ContextProvider; import org.dodgybits.shuffle.android.persistence.provider.ProjectProvider; import org.dodgybits.shuffle.android.preference.view.Progress; import org.dodgybits.shuffle.android.synchronisation.tracks.ApiException; import org.dodgybits.shuffle.android.synchronisation.tracks.model.TracksEntity; import org.dodgybits.shuffle.android.synchronisation.tracks.parsing.IContextLookup; import org.dodgybits.shuffle.android.synchronisation.tracks.parsing.IProjectLookup; import org.dodgybits.shuffle.android.synchronisation.tracks.parsing.Parser; import android.database.Cursor; import android.net.Uri; import android.provider.BaseColumns; import android.text.TextUtils; import android.util.Log; /** * Base class for handling synchronization, template method object. * * @author Morten Nielsen */ public abstract class Synchronizer<Entity extends TracksEntity> implements IProjectLookup,IContextLookup { private static final String cTag = "Synchronizer"; protected EntityPersister<Entity> mPersister; protected WebClient mWebClient; protected android.content.Context mContext; protected final TracksSynchronizer mTracksSynchronizer; private int mBasePercent; public Synchronizer( EntityPersister<Entity> persister, TracksSynchronizer tracksSynchronizer, WebClient client, android.content.Context context, int basePercent) { mPersister = persister; mTracksSynchronizer = tracksSynchronizer; mWebClient = client; mContext = context; mBasePercent = basePercent; } public void synchronize() throws ApiException { mTracksSynchronizer.reportProgress(Progress.createProgress(mBasePercent, readingLocalText())); Map<Id, Entity> localEntities = getShuffleEntities(); verifyEntitiesForSynchronization(localEntities); mTracksSynchronizer.reportProgress(Progress.createProgress(mBasePercent, readingRemoteText())); TracksEntities<Entity> tracksEntities = getTrackEntities(); mergeAlreadySynchronizedEntities(localEntities, tracksEntities); addNewEntitiesToShuffle(tracksEntities); mTracksSynchronizer.reportProgress(Progress.createProgress( mBasePercent + 33, stageFinishedText())); } private void mergeAlreadySynchronizedEntities( Map<Id, Entity> localEntities, TracksEntities<Entity> tracksEntities) { int startCounter = localEntities.size() + 1; int count = 0; for (Entity localEntity : localEntities.values()) { count++; mTracksSynchronizer.reportProgress(Progress.createProgress(calculatePercent(startCounter, count), processingText())); mergeSingle(tracksEntities, localEntity); } } private int calculatePercent(int startCounter, int count) { int percent = mBasePercent + Math.round(((count * 100) / startCounter) * 0.33f); return percent; } private void addNewEntitiesToShuffle(TracksEntities<Entity> tracksEntities) { for (Entity remoteEntity : tracksEntities.getEntities().values()) { insertEntity(remoteEntity); } } public Id findProjectIdByTracksId(Id tracksId) { return findEntityLocalIdByTracksId(tracksId, ProjectProvider.Projects.CONTENT_URI); } public Id findContextIdByTracksId(Id tracksId) { return findEntityLocalIdByTracksId(tracksId, ContextProvider.Contexts.CONTENT_URI); } protected Id findTracksIdByProjectId(Id projectId) { return findEntityTracksIdByLocalId(projectId, ProjectProvider.Projects.CONTENT_URI); } protected Id findTracksIdByContextId(Id contextId) { return findEntityTracksIdByLocalId(contextId, ContextProvider.Contexts.CONTENT_URI); } protected abstract void verifyEntitiesForSynchronization(Map<Id, Entity> localEntities); protected abstract String readingRemoteText(); protected abstract String processingText(); protected abstract String readingLocalText(); protected abstract String stageFinishedText(); protected abstract String entityIndexUrl(); protected abstract Entity createMergedLocalEntity(Entity localEntity, Entity newEntity); protected abstract String createEntityUrl(Entity localEntity); protected abstract String createDocumentForEntity(Entity localEntity); protected abstract EntityBuilder<Entity> createBuilder(); private TracksEntities<Entity> getTrackEntities() throws ApiException { String tracksEntityXml; try { WebResult result = mWebClient.getUrlContent(entityIndexUrl()); StatusLine status = result.getStatus(); if(status.getStatusCode() == HttpStatus.SC_OK) tracksEntityXml = result.getContent(); else throw new ApiException("Invalid response from server: " + status.toString()); } catch (ApiException e) { Log.w(cTag, e); throw e; } return getEntityParser().parseDocument(tracksEntityXml); } protected abstract Parser<Entity> getEntityParser(); private Id findEntityLocalIdByTracksId(Id tracksId, Uri contentUri) { Id id = Id.NONE; Cursor cursor = mContext.getContentResolver().query( contentUri, new String[] { BaseColumns._ID}, "tracks_id = ?", new String[] {tracksId.toString()}, null); if (cursor.moveToFirst()) { id = Id.create(cursor.getLong(0)); } cursor.close(); return id; } private Id findEntityTracksIdByLocalId(Id localId, Uri contentUri) { Id id = Id.NONE; Cursor cursor = mContext.getContentResolver().query( contentUri, new String[] { "tracks_id" }, BaseColumns._ID + " = ?", new String[] {localId.toString()}, null); if (cursor.moveToFirst()) { id = Id.create(cursor.getLong(0)); } cursor.close(); return id; } private void insertEntity(Entity entity) { mPersister.insert(entity); } private void updateEntity(Entity entity) { mPersister.update(entity); } protected boolean deleteEntity(Entity entity) { return mPersister.moveToTrash(entity.getLocalId()); } private Entity findEntityByLocalName(Collection<Entity> remoteEntities, Entity localEntity) { Entity foundEntity = null; for (Entity entity : remoteEntities) if (entity.getLocalName().equals(localEntity.getLocalName())) { foundEntity = entity; } return foundEntity; } private void mergeSingle(TracksEntities<Entity> tracksEntities, Entity localEntity) { final Map<Id, Entity> remoteEntities = tracksEntities.getEntities(); if (!localEntity.getTracksId().isInitialised()) { handleLocalEntityNotYetInTracks(localEntity, remoteEntities); return; } Entity remoteEntity = remoteEntities.get(localEntity.getTracksId()); if (remoteEntity != null) { mergeLocalAndRemoteEntityBasedOnModifiedDate(localEntity, remoteEntity); remoteEntities.remove(remoteEntity.getTracksId()); } else if (tracksEntities.isErrorFree()){ // only delete entities if we didn't encounter errors parsing deleteEntity(localEntity); } } private void handleLocalEntityNotYetInTracks(Entity localEntity, final Map<Id, Entity> remoteEntities) { Entity newEntity = findEntityByLocalName(remoteEntities.values(), localEntity); if (newEntity != null) { remoteEntities.remove(newEntity.getTracksId()); } else { newEntity = createEntityInTracks(localEntity); } if (newEntity != null) { updateEntity(createMergedLocalEntity(localEntity, newEntity)); } } private void mergeLocalAndRemoteEntityBasedOnModifiedDate(Entity localEntity, Entity remoteEntity) { final long remoteModified = remoteEntity.getModifiedDate(); final long localModified = localEntity.getModifiedDate(); if (remoteModified == localModified && remoteEntity.isDeleted() == localEntity.isDeleted()) return; if (remoteModified >= localModified) { updateEntity(createMergedLocalEntity(localEntity, remoteEntity)); } else { updateTracks(localEntity); } } private void updateTracks(Entity localEntity) { String document = createDocumentForEntity(localEntity); try { mWebClient.putContentToUrl(createEntityUrl(localEntity), document); } catch (ApiException e) { Log.w(cTag, "Failed to update entity in tracks " + localEntity + ":" + e.getMessage(), e); } } private Entity createEntityInTracks(Entity entity) { String document = createDocumentForEntity(entity); try { String location = mWebClient.postContentToUrl(entityIndexUrl(), document); if (!TextUtils.isEmpty(location.trim())) { Id id = parseIdFromLocation(location); EntityBuilder<Entity> builder = createBuilder(); builder.mergeFrom(entity); builder.setTracksId(id); entity = builder.build(); } } catch (ApiException e) { Log.w(cTag, "Failed to create entity in tracks " + entity + ":" + e.getMessage(), e); } return entity; } private Id parseIdFromLocation(String location) { String[] parts = location.split("/"); String document = parts[parts.length - 1]; long id = Long.parseLong( document ); return Id.create(id); } private Map<Id, Entity> getShuffleEntities() { Map<Id, Entity> list = new HashMap<Id, Entity>(); Cursor cursor = mContext.getContentResolver().query( mPersister.getContentUri(), mPersister.getFullProjection(), null, null, null); while (cursor.moveToNext()) { Entity entity = mPersister.read(cursor); list.put(entity.getLocalId(), entity); } cursor.close(); return list; } }