diff --git a/html/compressed_form.js b/html/compressed_form.js
index 1966925..5dad559 100644
--- a/html/compressed_form.js
+++ b/html/compressed_form.js
@@ -48,8 +48,8 @@ mobwrite.sniffUserAgent=function(){if(window.opera)mobwrite.UA_opera=true;else{v
mobwrite.minSyncInterval=1000;mobwrite.maxSyncInterval=10000;mobwrite.syncInterval=2000;mobwrite.idPrefix="";mobwrite.nullifyAll=false;mobwrite.clientChange_=false;mobwrite.serverChange_=false;mobwrite.syncAjaxObj_=null;mobwrite.uniqueId=function(){var b="abcdefghijklmnopqrstuvwxyz",c=b.charAt(Math.random()*b.length);b+="0123456789-_:.";for(var d=1;d<8;d++)c+=b.charAt(Math.random()*b.length);if(c.indexOf("--")!=-1)c=mobwrite.uniqueId();return c};mobwrite.syncUsername=mobwrite.uniqueId();
mobwrite.shared={};mobwrite.shareHandlers=[];mobwrite.shareObj=function(b){if(b){this.file=b;this.dmp=new diff_match_patch;this.dmp.Diff_Timeout=0.5;this.editStack=[];mobwrite.debug&&window.console.info('Creating shareObj: "'+b+'"')}};a=mobwrite.shareObj.prototype;a.shadowText="";a.clientVersion=0;a.serverVersion=0;a.deltaOk=false;a.mergeChanges=true;a.getClientText=function(){window.alert("Defined by subclass");return""};a.setClientText=function(){window.alert("Defined by subclass")};
a.patchClientText=function(b){var c=this.getClientText();b=this.dmp.patch_apply(b,c);c!=b[0]&&this.setClientText(b[0])};a.onSentDiff=function(){};a.fireChange=function(b){if("createEvent"in document){var c=document.createEvent("HTMLEvents");c.initEvent("change",false,false);b.dispatchEvent(c)}else"fireEvent"in b&&b.fireEvent("onchange")};a.nullify=function(){mobwrite.unshare(this.file);return"N:"+mobwrite.idPrefix+this.file+"\n"};
-a.syncText=function(){var b=this.getClientText();if(this.deltaOk){var c=this.dmp.diff_main(this.shadowText,b,true);if(c.length>2){this.dmp.diff_cleanupSemantic(c);this.dmp.diff_cleanupEfficiency(c)}var d=c.length!=1||c[0][0]!=0;if(d){mobwrite.clientChange_=true;this.shadowText=b}if(d||!this.editStack.length){b=(this.mergeChanges?"d:":"D:")+this.clientVersion+":"+this.dmp.diff_toDelta(c);this.editStack.push([this.clientVersion,b]);this.clientVersion++;this.onSentDiff(c)}}else{if(this.shadowText!=b)this.shadowText=
-b;this.clientVersion++;b="r:"+this.clientVersion+":"+encodeURI(b).replace(/%20/g," ");this.editStack.push([this.clientVersion,b]);this.deltaOk=true}c="F:"+this.serverVersion+":"+mobwrite.idPrefix+this.file+"\n";for(b=0;b2){this.dmp.diff_cleanupSemantic(c);this.dmp.diff_cleanupEfficiency(c)}var d=c.length!=1||c[0][0]!=0;if(d){mobwrite.clientChange_=true;this.shadowText=b}if(d||!this.editStack.length){b=(this.mergeChanges?"d:":"D:")+this.clientVersion+":"+this.dmp.diff_toDelta(c);this.editStack.push([this.clientVersion,b]);this.clientVersion++;this.onSentDiff(c)}}else{this.shadowText=b;this.clientVersion++;
+b="r:"+this.clientVersion+":"+encodeURI(b).replace(/%20/g," ");this.editStack.push([this.clientVersion,b]);this.deltaOk=true}c="F:"+this.serverVersion+":"+mobwrite.idPrefix+this.file+"\n";for(b=0;b dict = new HashMap();
- try {
- BufferedReader input = new BufferedReader(new FileReader(tmpfile));
-
- try {
- String line;
- Pattern lineRegex = Pattern.compile("^(\\w+)\\s*=\\s*(.+)$");
- while ((line = input.readLine()) != null) {
- line = line.trim();
- // Comment lines start with a ;
- if (!line.startsWith(";")) {
- Matcher m = lineRegex.matcher(line);
- if (m.matches()) {
- String name = m.group(1);
- String value = m.group(2);
- name = URLDecoder.decode(name, "UTF-8");
- value = URLDecoder.decode(value, "UTF-8");
- dict.put(name, value);
- }
- }
- }
- }
- finally {
- input.close();
- }
- } catch (FileNotFoundException ex){
- // No config to load.
- } catch (IOException ex){
- ex.printStackTrace();
- }
-
- // Set each of the configuration parameters.
- String value;
- value = dict.get(Fields.SyncGateway.toString());
- if (value != null) {
- this.mobwrite.syncGateway = value;
- }
- value = dict.get(Fields.SyncUsername.toString());
- if (value != null) {
- this.mobwrite.syncUsername = value;
- }
-
- String id = dict.get(Fields.ID.toString());
- String filename = dict.get(Fields.Filename.toString());
- if (id != null && filename != null) {
- this.shareFile = new ShareFile(filename, id);
- value = dict.get(Fields.EditStack.toString());
- if (value != null) {
- // Decode the edit stack.
- this.shareFile.editStack.clear();
- String[] lines = value.split("\n");
- Integer version = null;
- for (String line : lines) {
- // Even lines are version numbers, odd lines are action strings.
- if (version == null) {
- version = Integer.getInteger(line);
- } else {
- this.shareFile.editStack.push(new Object[]{version, line});
- version = null;
- }
- }
- }
- value = dict.get(Fields.ShadowText.toString());
- if (value != null) {
- this.shareFile.shadowText = value;
- }
- value = dict.get(Fields.ClientVersion.toString());
- if (value != null) {
- this.shareFile.clientVersion = Integer.valueOf(value);
- }
- value = dict.get(Fields.ServerVersion.toString());
- if (value != null) {
- this.shareFile.serverVersion = Integer.valueOf(value);
- }
- value = dict.get(Fields.DeltaOk.toString());
- if (value != null) {
- this.shareFile.deltaOk = Boolean.parseBoolean(value);
- }
- value = dict.get(Fields.MergeChanges.toString());
- if (value != null) {
- this.shareFile.mergeChanges = Boolean.parseBoolean(value);
- }
- }
- }
-
- private enum Fields {
- SyncGateway, SyncUsername, Filename, ID, EditStack, ShadowText,
- ClientVersion, ServerVersion, DeltaOk, MergeChanges
- }
-
- /**
- * Usage: FileSharer
- * @param args Command line arguments.
- */
- public static void main(String[] args) {
- if (args.length != 3) {
- System.err.println("Usage: FileSharer ");
- return;
- }
- String syncgateway = args[0];
- String docname = args[1];
- String filename = args[2];
- String configname = filename + "." + docname + ".mobwrite";
-
- FileSharer sharer = new FileSharer();
- sharer.mobwrite.syncGateway = syncgateway;
-
- sharer.loadConfig(configname);
- if (sharer.shareFile == null || !args[1].equals(sharer.shareFile.file)) {
- // Provided ID is different from the one we loaded (or none was loaded).
- sharer.shareFile = sharer.new ShareFile(filename, docname);
- }
-
- // Add the ShareFile to MobWrite and sync once.
- sharer.mobwrite.share(sharer.shareFile);
-
- sharer.saveConfig(configname);
- }
-
-}
+/**
+ * FileSharer - Sharing a document via MobWrite
+ *
+ * Copyright 2009 Google Inc.
+ * http://code.google.com/p/google-mobwrite/
+ *
+ * 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.mobwrite;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.logging.Level;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+public class FileSharer {
+
+ /**
+ * @author fraser
+ *
+ */
+ private class MobWriteSingleExec extends MobWriteClient {
+ /**
+ * Constructor. Initializes a MobWrite client.
+ * @param syncGateway URL of the server.
+ */
+ public MobWriteSingleExec(String syncGateway) {
+ super(syncGateway);
+ }
+
+ /**
+ * Share the specified object(s) for one cycle.
+ */
+ public void share(ShareObj ... shareObjs) {
+ for (int i = 0; i < shareObjs.length; i++) {
+ ShareObj shareObj = shareObjs[i];
+ shareObj.mobwrite = this;
+ this.shared.put(shareObj.file, shareObj);
+ this.logger.log(Level.INFO, "Sharing shareObj: \"" + shareObj.file + "\"");
+ }
+
+ syncRun1_();
+
+ for (int i = 0; i < shareObjs.length; i++) {
+ ShareObj shareObj = this.shared.remove(shareObjs[i].file);
+ if (shareObj != null) {
+ shareObj.mobwrite = null;
+ this.logger.log(Level.INFO, "Unshared: \"" + shareObj.file + "\"");
+ }
+ }
+ }
+ }
+
+ private class ShareFile extends ShareObj {
+
+ /**
+ * The name of the file on disk to be shared.
+ */
+ private String filename;
+
+ /**
+ * Constructor of shared object representing a file.
+ * @param filename Filename of file on disk.
+ * @param docname Document name to share as.
+ */
+ public ShareFile(String filename, String docname) {
+ super(docname);
+ this.filename = filename;
+ }
+
+ /**
+ * Retrieve the file contents.
+ * @return Plaintext content.
+ */
+ public String getClientText() {
+ StringBuilder sb = new StringBuilder();
+ BufferedReader input;
+ try {
+ input = new BufferedReader(new FileReader(filename));
+
+ try {
+ String line;
+ while ((line = input.readLine()) != null) {
+ sb.append(line);
+ }
+ }
+ finally {
+ input.close();
+ }
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ return null;
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Set the file contents.
+ * @param text New content.
+ */
+ @Override
+ public void setClientText(String text) {
+ try {
+ BufferedWriter output = new BufferedWriter(new FileWriter(filename));
+
+ try {
+ output.write(text);
+ } finally {
+ output.close();
+ }
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ }
+ }
+ }
+
+
+ /**
+ * Instance of MobWrite.
+ */
+ private MobWriteSingleExec mobwrite;
+
+ /**
+ * Instance of ShareFile, a ShareObj.
+ */
+ private ShareFile shareFile;
+
+ /**
+ * Constructor for FileSharer.
+ * Creates a MobWrite client.
+ * @param syncGateway URL of the server.
+ */
+ public FileSharer(String syncGateway) {
+ mobwrite = new MobWriteSingleExec(syncGateway);
+ }
+
+ /**
+ * Save MobWrite's current state to disk.
+ */
+ private void saveConfig(String tmpfile) {
+ try {
+ BufferedWriter output = new BufferedWriter(new FileWriter(tmpfile));
+
+ try {
+ writeOneConfigLine(output, Fields.SyncGateway, this.mobwrite.getSyncGateway());
+ writeOneConfigLine(output, Fields.SyncUsername, this.mobwrite.syncUsername);
+ writeOneConfigLine(output, Fields.Filename, this.shareFile.filename);
+ writeOneConfigLine(output, Fields.ID, this.shareFile.file);
+ StringBuilder sb = new StringBuilder();
+ for (Object[] pair : this.shareFile.editStack) {
+ sb.append(String.valueOf(pair[0])).append('\n');
+ sb.append((String) pair[1]).append('\n');
+ }
+ writeOneConfigLine(output, Fields.EditStack, sb.toString());
+ writeOneConfigLine(output, Fields.ShadowText, this.shareFile.shadowText);
+ writeOneConfigLine(output, Fields.ClientVersion, Integer.toString(this.shareFile.clientVersion));
+ writeOneConfigLine(output, Fields.ServerVersion, Integer.toString(this.shareFile.serverVersion));
+ writeOneConfigLine(output, Fields.DeltaOk, Boolean.toString(this.shareFile.deltaOk));
+ writeOneConfigLine(output, Fields.MergeChanges, Boolean.toString(this.shareFile.mergeChanges));
+ } finally {
+ output.close();
+ }
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ }
+ }
+
+ /**
+ * Write one "name = value" line, properly encoded.
+ * @param output The output writer.
+ * @param field Name of tuple.
+ * @param value Value of tuple.
+ * @throws IOException
+ */
+ private void writeOneConfigLine(BufferedWriter output, Fields field,
+ String value) throws IOException {
+ String name = URLEncoder.encode(field.toString(), "UTF-8");
+ value = URLEncoder.encode(value, "UTF-8");
+ output.write(name + " = " + value + "\n");
+ }
+
+ /**
+ * Load MobWrite's current state from disk.
+ */
+ private void loadConfig(String tmpfile) {
+ HashMap dict = new HashMap();
+ try {
+ BufferedReader input = new BufferedReader(new FileReader(tmpfile));
+
+ try {
+ String line;
+ Pattern lineRegex = Pattern.compile("^(\\w+)\\s*=\\s*(.+)$");
+ while ((line = input.readLine()) != null) {
+ line = line.trim();
+ // Comment lines start with a ;
+ if (!line.startsWith(";")) {
+ Matcher m = lineRegex.matcher(line);
+ if (m.matches()) {
+ String name = m.group(1);
+ String value = m.group(2);
+ name = URLDecoder.decode(name, "UTF-8");
+ value = URLDecoder.decode(value, "UTF-8");
+ dict.put(name, value);
+ }
+ }
+ }
+ }
+ finally {
+ input.close();
+ }
+ } catch (FileNotFoundException ex){
+ // No config to load.
+ } catch (IOException ex){
+ ex.printStackTrace();
+ }
+
+ // Set each of the configuration parameters.
+ // Ignore the syncGateway parameter, it was already specified on the command line.
+ String value;
+ value = dict.get(Fields.SyncUsername.toString());
+ if (value != null) {
+ this.mobwrite.syncUsername = value;
+ }
+
+ String id = dict.get(Fields.ID.toString());
+ String filename = dict.get(Fields.Filename.toString());
+ if (id != null && filename != null) {
+ this.shareFile = new ShareFile(filename, id);
+ value = dict.get(Fields.EditStack.toString());
+ if (value != null) {
+ // Decode the edit stack.
+ this.shareFile.editStack.clear();
+ String[] lines = value.split("\n");
+ Integer version = null;
+ for (String line : lines) {
+ // Even lines are version numbers, odd lines are action strings.
+ if (version == null) {
+ version = Integer.getInteger(line);
+ } else {
+ this.shareFile.editStack.push(new Object[]{version, line});
+ version = null;
+ }
+ }
+ }
+ value = dict.get(Fields.ShadowText.toString());
+ if (value != null) {
+ this.shareFile.shadowText = value;
+ }
+ value = dict.get(Fields.ClientVersion.toString());
+ if (value != null) {
+ this.shareFile.clientVersion = Integer.valueOf(value);
+ }
+ value = dict.get(Fields.ServerVersion.toString());
+ if (value != null) {
+ this.shareFile.serverVersion = Integer.valueOf(value);
+ }
+ value = dict.get(Fields.DeltaOk.toString());
+ if (value != null) {
+ this.shareFile.deltaOk = Boolean.parseBoolean(value);
+ }
+ value = dict.get(Fields.MergeChanges.toString());
+ if (value != null) {
+ this.shareFile.mergeChanges = Boolean.parseBoolean(value);
+ }
+ }
+ }
+
+ private enum Fields {
+ SyncGateway, SyncUsername, Filename, ID, EditStack, ShadowText,
+ ClientVersion, ServerVersion, DeltaOk, MergeChanges
+ }
+
+ /**
+ * Usage: FileSharer
+ * @param args Command line arguments.
+ */
+ public static void main(String[] args) {
+ if (args.length != 3) {
+ System.err.println("Usage: FileSharer ");
+ return;
+ }
+ String syncgateway = args[0];
+ String docname = args[1];
+ String filename = args[2];
+ String configname = filename + "." + docname + ".mobwrite";
+
+ FileSharer sharer = new FileSharer(syncgateway);
+
+ sharer.loadConfig(configname);
+ if (sharer.shareFile == null || !args[1].equals(sharer.shareFile.file)) {
+ // Provided ID is different from the one we loaded (or none was loaded).
+ sharer.shareFile = sharer.new ShareFile(filename, docname);
+ }
+
+ // Add the ShareFile to MobWrite and sync once.
+ sharer.mobwrite.share(sharer.shareFile);
+
+ sharer.saveConfig(configname);
+ }
+
+}
diff --git a/java/com/google/mobwrite/MobWriteClient.java b/java/com/google/mobwrite/MobWriteClient.java
index e52dce7..c9df44d 100644
--- a/java/com/google/mobwrite/MobWriteClient.java
+++ b/java/com/google/mobwrite/MobWriteClient.java
@@ -27,6 +27,7 @@
import java.net.URLConnection;
import java.net.URLDecoder;
import java.net.URLEncoder;
+import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
@@ -41,11 +42,6 @@
* Class representing a MobWrite client.
*/
public class MobWriteClient {
- /**
- * URL of web gateway.
- */
- public String syncGateway = "http://mobwrite3.appspot.com/scripts/q.py";
-
/**
* Time to wait for a connection before giving up and retrying.
*/
@@ -98,51 +94,78 @@ public class MobWriteClient {
protected String syncUsername;
/**
- * Hash of all shared objects.
+ * URL of web gateway.
*/
- protected Map shared;
+ private String syncGateway;
/**
- * Logging object.
+ * Hash of all shared objects.
*/
- protected Logger logger;
+ protected Map shared;
/**
* Currently running synchronization thread.
*/
private Thread syncThread = null;
+ /**
+ * Number of digits in the username.
+ */
+ private static final int IDSIZE = 8;
+
+ /**
+ * Logging object.
+ */
+ protected static final Logger logger = Logger.getLogger("MobWrite");
+
+ /**
+ * Cryptographically strong pseudo-random number generator.
+ */
+ private static final SecureRandom RANDOM = new SecureRandom();
+
/**
* Constructor. Initializes a MobWrite client.
+ * @param syncGateway URL of the server.
*/
- public MobWriteClient() {
- this.logger = Logger.getLogger("MobWrite");
- this.syncUsername = MobWriteClient.uniqueId();
+ public MobWriteClient(String syncGateway) {
+ this.syncUsername = MobWriteClient.uniqueId(IDSIZE);
+ this.syncGateway = syncGateway;
logger.log(Level.INFO, "Username: " + this.syncUsername);
+ logger.log(Level.INFO, "Gateway: " + this.syncGateway);
this.shared = new HashMap();
}
/**
- * Return a random id that's 8 letters long.
- * 26*(26+10+4)^7 = 4,259,840,000,000
+ * Return the URL of the server.
+ * @return URL of the server.
+ */
+ public String getSyncGateway() {
+ return this.syncGateway;
+ }
+
+
+ /**
+ * Return a random id.
+ * For size = 8: 26*(26+10+4)^7 = 4,259,840,000,000
+ * @param size The number of characters to use.
* @return Random id.
*/
- public static String uniqueId() {
+ public static String uniqueId(int size) {
// First character must be a letter.
// IE is case insensitive (in violation of the W3 spec).
String soup = "abcdefghijklmnopqrstuvwxyz";
StringBuffer sb = new StringBuffer();
- sb.append(soup.charAt((int) (Math.random() * soup.length())));
+ sb.append(soup.charAt(RANDOM.nextInt(soup.length())));
// Subsequent characters may include these.
soup += "0123456789-_:.";
- for (int x = 1; x < 8; x++) {
- sb.append(soup.charAt((int) (Math.random() * soup.length())));
+ for (int x = 1; x < size; x++) {
+ sb.append(soup.charAt(RANDOM.nextInt(soup.length())));
}
String id = sb.toString();
// Don't allow IDs with '--' in them since it might close a comment.
if (id.indexOf("--") != -1) {
- id = uniqueId();
+ id = uniqueId(size);
}
return id;
// Getting the maximum possible density in the ID is worth the extra code,
@@ -219,6 +242,7 @@ protected void syncRun1_() {
/**
* Parse all server-side changes and distribute them to the shared objects.
+ * @param text The commands from the server to parse and execute.
*/
private void syncRun2_(String text) {
// Initialize serverChange_, to be checked at the end of syncRun2_.
@@ -232,8 +256,7 @@ private void syncRun2_(String text) {
String[] lines = text.split("\n");
ShareObj file = null;
int clientVersion = -1;
- for (int i = 0; i < lines.length; i++) {
- String line = lines[i];
+ for (String line : lines) {
if (line.isEmpty()) {
// Terminate on blank line.
break;
@@ -281,7 +304,7 @@ private void syncRun2_(String text) {
// Remove any elements from the edit stack with low version numbers
// which have been acked by the server.
Iterator