/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.sync; import java.io.IOException; import java.net.URISyntaxException; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.json.simple.parser.ParseException; import org.mozilla.gecko.sync.MetaGlobalException.MetaGlobalMalformedSyncIDException; import org.mozilla.gecko.sync.MetaGlobalException.MetaGlobalMalformedVersionException; import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate; import org.mozilla.gecko.sync.net.SyncStorageRecordRequest; import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate; import org.mozilla.gecko.sync.net.SyncStorageResponse; public class MetaGlobal implements SyncStorageRequestDelegate { private static final String LOG_TAG = "MetaGlobal"; protected String metaURL; protected String credentials; // Fields. protected ExtendedJSONObject engines; protected Long storageVersion; protected String syncID; // Lookup tables. protected Map<String, String> syncIDs; protected Map<String, Integer> versions; protected Map<String, MetaGlobalException> exceptions; // Temporary location to store our callback. private MetaGlobalDelegate callback; // A little hack so we can use the same delegate implementation for upload and download. private boolean isUploading; public MetaGlobal(String metaURL, String credentials) { this.metaURL = metaURL; this.credentials = credentials; } public void fetch(MetaGlobalDelegate delegate) { this.callback = delegate; try { this.isUploading = false; SyncStorageRecordRequest r = new SyncStorageRecordRequest(this.metaURL); r.delegate = this; r.deferGet(); } catch (URISyntaxException e) { this.callback.handleError(e); } } public void upload(MetaGlobalDelegate callback) { try { this.isUploading = true; SyncStorageRecordRequest r = new SyncStorageRecordRequest(this.metaURL); r.delegate = this; this.callback = callback; r.put(this.asCryptoRecord()); } catch (Exception e) { callback.handleError(e); } } protected ExtendedJSONObject asRecordContents() { ExtendedJSONObject json = new ExtendedJSONObject(); json.put("storageVersion", storageVersion); json.put("engines", engines); json.put("syncID", syncID); return json; } /** * Return a copy ready for upload. * @return an unencrypted <code>CryptoRecord</code>. */ public CryptoRecord asCryptoRecord() { ExtendedJSONObject payload = this.asRecordContents(); CryptoRecord record = new CryptoRecord(payload); record.collection = "meta"; record.guid = "global"; record.deleted = false; return record; } public void setFromRecord(CryptoRecord record) throws IllegalStateException, IOException, ParseException, NonObjectJSONException { Logger.debug(LOG_TAG, "meta/global is " + record.payload.toJSONString()); this.storageVersion = (Long) record.payload.get("storageVersion"); this.syncID = (String) record.payload.get("syncID"); setEngines(record.payload.getObject("engines")); } public Long getStorageVersion() { return this.storageVersion; } public void setStorageVersion(Long version) { this.storageVersion = version; } public ExtendedJSONObject getEngines() { return engines; } public void setEngines(ExtendedJSONObject engines) { if (engines == null) { engines = new ExtendedJSONObject(); } this.engines = engines; final int count = engines.size(); versions = new HashMap<String, Integer>(count); syncIDs = new HashMap<String, String>(count); exceptions = new HashMap<String, MetaGlobalException>(count); for (String engineName : engines.keySet()) { try { ExtendedJSONObject engineEntry = engines.getObject(engineName); recordEngineState(engineName, engineEntry); } catch (NonObjectJSONException e) { Logger.error(LOG_TAG, "Engine field for " + engineName + " in meta/global is not an object."); recordEngineState(engineName, new ExtendedJSONObject()); // Doesn't have a version or syncID, for example, so will be server wiped. } } } /** * Take a JSON object corresponding to the 'engines' field for the provided engine name, * updating {@link #syncIDs} and {@link #versions} accordingly. * * If the record is malformed, an entry is added to {@link #exceptions}, to be rethrown * during validation. */ protected void recordEngineState(String engineName, ExtendedJSONObject engineEntry) { if (engineEntry == null) { throw new IllegalArgumentException("engineEntry cannot be null."); } // Record syncID first, so that engines with bad versions are recorded. try { String syncID = engineEntry.getString("syncID"); if (syncID == null) { Logger.warn(LOG_TAG, "No syncID for " + engineName + ". Recording exception."); exceptions.put(engineName, new MetaGlobalMalformedSyncIDException()); } syncIDs.put(engineName, syncID); } catch (ClassCastException e) { // Malformed syncID on the server. Wipe the server. Logger.warn(LOG_TAG, "Malformed syncID " + engineEntry.get("syncID") + " for " + engineName + ". Recording exception."); exceptions.put(engineName, new MetaGlobalMalformedSyncIDException()); } try { Integer version = engineEntry.getIntegerSafely("version"); Logger.trace(LOG_TAG, "Engine " + engineName + " has server version " + version); if (version == null || version.intValue() == 0) { // Invalid version. Wipe the server. Logger.warn(LOG_TAG, "Malformed version " + version + " for " + engineName + ". Recording exception."); exceptions.put(engineName, new MetaGlobalMalformedVersionException()); return; } versions.put(engineName, version); } catch (NumberFormatException e) { // Invalid version. Wipe the server. Logger.warn(LOG_TAG, "Malformed version " + engineEntry.get("version") + " for " + engineName + ". Recording exception."); exceptions.put(engineName, new MetaGlobalMalformedVersionException()); return; } } /** * Get enabled engine names. * * @return a collection of engine names or <code>null</code> if meta/global * was malformed. */ public Set<String> getEnabledEngineNames() { if (engines == null) { return null; } return new HashSet<String>(engines.keySet()); } /** * Returns if the server settings and local settings match. * Throws a specific MetaGlobalException if that's not the case. */ public void verifyEngineSettings(String engineName, EngineSettings engineSettings) throws MetaGlobalException { // We use syncIDs as our canary. if (syncIDs == null) { throw new IllegalStateException("No meta/global record yet processed."); } if (engineSettings == null) { throw new IllegalArgumentException("engineSettings cannot be null."); } // First, see if we had a parsing problem. final MetaGlobalException exception = exceptions.get(engineName); if (exception != null) { throw exception; } final String syncID = syncIDs.get(engineName); if (syncID == null) { // We have checked engineName against enabled engine names before this, so // we should either have a syncID or an exception for this engine already. throw new IllegalArgumentException("Unknown engine " + engineName); } // Since we don't have an exception, and we do have a syncID, we should have a version. final Integer version = versions.get(engineName); if (version > engineSettings.version) { // We're out of date. throw new MetaGlobalException.MetaGlobalStaleClientVersionException(version); } if (!syncID.equals(engineSettings.syncID)) { // Our syncID is wrong. Reset client and take the server syncID. throw new MetaGlobalException.MetaGlobalStaleClientSyncIDException(syncID); } } public String getSyncID() { return syncID; } public void setSyncID(String syncID) { this.syncID = syncID; } // SyncStorageRequestDelegate methods for fetching. public String credentials() { return this.credentials; } public String ifUnmodifiedSince() { return null; } public void handleRequestSuccess(SyncStorageResponse response) { if (this.isUploading) { this.handleUploadSuccess(response); } else { this.handleDownloadSuccess(response); } } private void handleUploadSuccess(SyncStorageResponse response) { this.callback.handleSuccess(this, response); } private void handleDownloadSuccess(SyncStorageResponse response) { if (response.wasSuccessful()) { try { CryptoRecord record = CryptoRecord.fromJSONRecord(response.jsonObjectBody()); this.setFromRecord(record); this.callback.handleSuccess(this, response); } catch (Exception e) { this.callback.handleError(e); } return; } this.callback.handleFailure(response); } public void handleRequestFailure(SyncStorageResponse response) { if (response.getStatusCode() == 404) { this.callback.handleMissing(this, response); return; } this.callback.handleFailure(response); } public void handleRequestError(Exception e) { this.callback.handleError(e); } }