├── .gitignore ├── example ├── pubspec.yaml ├── web │ ├── demo_client.html │ ├── demo_client.css │ └── demo_client.dart └── bin │ └── demo_server.dart ├── pubspec.yaml ├── README.md ├── test └── test.dart ├── lib ├── connection.dart ├── collab.dart ├── server │ ├── connection.dart │ └── server.dart ├── client │ ├── connection.dart │ ├── web_utils.dart │ └── web_client.dart ├── utils.dart ├── operation.dart ├── messages.dart ├── message.dart ├── text │ └── text.dart └── document.dart └── LICENSE.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.dart.js 2 | *.dart.js.map 3 | packages 4 | pubspec.lock 5 | .project 6 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: collab_example 2 | description: dart-collab demo application 3 | dependencies: 4 | browser: any 5 | collab: 6 | path: ../ 7 | unittest: any 8 | logging: any 9 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: collab 2 | description: A library for building real-time collaborative apps using operational tranforms. 3 | dependencies: 4 | browser: any 5 | unittest: any 6 | logging: any 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dart-collab 2 | =========== 3 | 4 | An operational transform library for Dart. 5 | 6 | dart-collab allows multiple clients to edit a single document concurrently, 7 | while keeping them in sync. The library provides message, operation, document, 8 | server and client classes used to built a collaborative editing system. 9 | -------------------------------------------------------------------------------- /example/web/demo_client.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | dart-collab Demo 6 | 7 | 8 | 9 | 10 |

dart-collab Demo

11 | 12 |
13 |
14 |
disconnected
15 |
16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // TODO: 16 | // Unit tests 17 | // client/server integration test. 18 | // OT scenarios across two clients with simultaneous operations. 19 | main() { 20 | } 21 | -------------------------------------------------------------------------------- /lib/connection.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | part of collab; 16 | 17 | abstract class Connection { 18 | Stream get stream; 19 | void add(String message); 20 | void addStream(Stream message); 21 | void close(); 22 | } 23 | -------------------------------------------------------------------------------- /lib/collab.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library collab; 16 | 17 | import 'dart:async'; 18 | import 'dart:convert' show JSON; 19 | import 'utils.dart'; 20 | 21 | part 'connection.dart'; 22 | part 'message.dart'; 23 | part 'messages.dart'; 24 | part 'document.dart'; 25 | part 'operation.dart'; 26 | -------------------------------------------------------------------------------- /example/web/demo_client.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #e0e0e0; 3 | font-family: Arial, Helvetica; 4 | } 5 | 6 | h1 { 7 | margin: 24px; 8 | } 9 | 10 | #container { 11 | width: 600px; 12 | margin: auto; 13 | } 14 | 15 | #editor { 16 | width: 100%; 17 | height: 400px; 18 | box-sizing: border-box; 19 | background: #eee; 20 | color: 222; 21 | font-size: 24px; 22 | padding: 8px; 23 | } 24 | 25 | #editor:focus { 26 | outline: solid 1px #abf; 27 | } 28 | 29 | #toolbar { 30 | background: #444; 31 | padding: 6px; 32 | height: 20px; 33 | border-radius: 4px 4px 0 0; 34 | } 35 | 36 | #status { 37 | color: #ddd; 38 | display: inline-block; 39 | border-radius: 4px; 40 | padding: 4px 8px; 41 | font-size: 10px; 42 | text-shadow: 1px 1px 1px rgba(0, 0, 0, .8); 43 | } 44 | 45 | #status.disconnected { 46 | background: red; 47 | } 48 | 49 | #status.connecting { 50 | background: #fa0; 51 | } 52 | 53 | #status.connected { 54 | background: #0a0; 55 | } -------------------------------------------------------------------------------- /lib/server/connection.dart: -------------------------------------------------------------------------------- 1 | part of server; 2 | 3 | // Copyright 2013 Google Inc. All Rights Reserved. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | class WebSocketConnection implements Connection { 18 | final WebSocket _socket; 19 | 20 | WebSocketConnection(WebSocket this._socket); 21 | 22 | Stream get stream => _socket; 23 | void add(String json) => _socket.add(json); 24 | void addStream(Stream stream) { _socket.addStream(stream); } 25 | void close() { _socket.close(); } 26 | } 27 | -------------------------------------------------------------------------------- /lib/client/connection.dart: -------------------------------------------------------------------------------- 1 | part of web_client; 2 | 3 | // Copyright 2013 Google Inc. All Rights Reserved. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | class WebSocketConnection implements Connection { 18 | final html.WebSocket _socket; 19 | 20 | WebSocketConnection(html.WebSocket this._socket); 21 | 22 | Stream get stream => _socket.onMessage 23 | .where((msg) => msg is html.MessageEvent) 24 | .map((msg) => msg.data); 25 | void add(String message) => _socket.send(message); 26 | void addStream(Stream stream) { 27 | stream.listen((msg) => _socket.send(msg)); 28 | } 29 | void close() => _socket.close(); 30 | } 31 | -------------------------------------------------------------------------------- /lib/utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library utils; 16 | 17 | import 'dart:math'; 18 | 19 | const String ALPHABET = 20 | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 21 | 22 | final Random _random = new Random(); 23 | 24 | String randomId() { 25 | StringBuffer sb = new StringBuffer(); 26 | for (int i = 0; i < 12; i++) { 27 | sb.write(ALPHABET[_random.nextInt(ALPHABET.length)]); 28 | } 29 | return sb.toString(); 30 | } 31 | 32 | Map mergeMaps(Map a, Map b) { 33 | Map merged = (a == null) ? new Map() : new Map.from(a); 34 | b.forEach((k, v) { merged[k] = v; }); 35 | return merged; 36 | } 37 | -------------------------------------------------------------------------------- /lib/operation.dart: -------------------------------------------------------------------------------- 1 | part of collab; 2 | 3 | // Copyright 2011 Google Inc. All Rights Reserved. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | /* 18 | * Operations modify a document. 19 | */ 20 | abstract class Operation extends Message { 21 | final String docId; 22 | // set when op created to the doc version of the client updated when 23 | // operations from this client that are ahead of this op are applied 24 | int docVersion; 25 | // set when an operation is applied by the server 26 | int sequence; 27 | 28 | Operation(String type, String senderId, this.docId, this.docVersion) 29 | : super(type, senderId); 30 | 31 | Operation.fromMap(Map map) 32 | : super.fromMap(map), 33 | docId = map['docId'], 34 | docVersion = map['docVersion'], 35 | sequence = map['sequence']; 36 | 37 | Map toMap([values]) => super.toMap(mergeMaps(values, { 38 | 'docId': docId, 'docVersion': docVersion, 'sequence': sequence})); 39 | 40 | void apply(Document document); 41 | } 42 | 43 | typedef Transform(Operation op1, Operation op2); 44 | -------------------------------------------------------------------------------- /lib/messages.dart: -------------------------------------------------------------------------------- 1 | part of collab; 2 | 3 | // Copyright 2011 Google Inc. All Rights Reserved. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | 18 | /** 19 | * Tells a client what its id is after connecting. 20 | */ 21 | class ClientIdMessage extends Message { 22 | static String TYPE = "clientId"; 23 | 24 | String clientId; 25 | 26 | ClientIdMessage(String senderId, this.clientId) : super(TYPE, senderId); 27 | 28 | ClientIdMessage.fromMap(Map map) 29 | : super.fromMap(map), 30 | clientId = map['clientId']; 31 | 32 | toMap([values]) => super.toMap(mergeMaps(values, {'clientId': clientId})); 33 | 34 | String toString() => "ClientIdMessage: $clientId"; 35 | } 36 | 37 | /** 38 | * Logs a simple message. Used for development and debugging. 39 | */ 40 | class LogMessage extends Message { 41 | static String TYPE = "log"; 42 | 43 | final String text; 44 | 45 | LogMessage(String senderId, this.text) : super(TYPE, senderId); 46 | 47 | LogMessage.fromMap(Map map) 48 | : super.fromMap(map), 49 | text = map['text']; 50 | 51 | toMap([values]) => super.toMap(mergeMaps(values, {'text': text})); 52 | } 53 | -------------------------------------------------------------------------------- /example/web/demo_client.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library demo_client; 16 | 17 | import 'dart:html'; 18 | import 'package:collab/client/web_client.dart'; 19 | import 'package:collab/client/web_utils.dart'; 20 | import 'package:collab/collab.dart' as collab; 21 | import 'package:collab/text/text.dart'; 22 | 23 | TextAreaElement editor; 24 | collab.Document doc; 25 | Element statusDiv; 26 | 27 | void main() { 28 | print("dart-collab demo"); 29 | editor = querySelector('#editor'); 30 | statusDiv = querySelector('#status'); 31 | doc = new TextDocument("test"); 32 | String host = window.location.hostname; 33 | print("host: $host"); 34 | 35 | var webSocket = new WebSocket("ws://$host:8080/connect"); 36 | var connection = new WebSocketConnection(webSocket); 37 | var client = new CollabWebClient(connection, doc); 38 | client.addStatusHandler(onStatusChange); 39 | makeEditable(editor, client); 40 | } 41 | 42 | void onStatusChange(int status) { 43 | switch (status) { 44 | case DISCONNECTED: 45 | case ERROR: 46 | statusDiv.classes.remove("connected"); 47 | statusDiv.classes.remove("connecting"); 48 | statusDiv.classes.add("disconnected"); 49 | statusDiv.text = "disconnected"; 50 | break; 51 | case CONNECTING: 52 | statusDiv.classes.remove("connected"); 53 | statusDiv.classes.add("connecting"); 54 | statusDiv.classes.remove("disconnected"); 55 | statusDiv.text = "connecting"; 56 | break; 57 | case CONNECTED: 58 | statusDiv.classes.add("connected"); 59 | statusDiv.classes.remove("connecting"); 60 | statusDiv.classes.remove("disconnected"); 61 | statusDiv.text = "connected"; 62 | break; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/message.dart: -------------------------------------------------------------------------------- 1 | part of collab; 2 | 3 | // Copyright 2011 Google Inc. All Rights Reserved. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | 18 | /** 19 | * Defines the basic [Message] class. 20 | */ 21 | 22 | String SERVER_ID = "_server"; 23 | 24 | /** 25 | * Messages are sent between clients and servers. 26 | */ 27 | class Message { 28 | final String id; 29 | final String senderId; 30 | // id of the messgage this is in reply to. can be null. 31 | final String replyTo; 32 | final String type; 33 | 34 | Message(this.type, this.senderId, [String replyTo]) 35 | : id = randomId(), 36 | this.replyTo = replyTo; 37 | 38 | Message.fromMap(Map map) 39 | : id = map['id'], 40 | senderId = map['senderId'], 41 | replyTo = map['replyTo'], 42 | type = map['type']; 43 | 44 | /** 45 | * Returns a JSON representation of the message. 46 | */ 47 | String get json => JSON.encode(toMap()); 48 | 49 | String toString() => "Message $json"; 50 | 51 | /** 52 | * Returns a [JSON.stringify] or Isolate SendPort compatible map 53 | * of String-> bool, String, num, List, Map. 54 | * 55 | * [values] is merged into the result so that subclasses can call toMap() with 56 | * additional values. 57 | */ 58 | Map toMap([Map values]) { 59 | Map m = mergeMaps(values, {'type': type, 'id': id, 'senderId': senderId}); 60 | if (replyTo != null) { 61 | m['replyTo'] = replyTo; 62 | } 63 | return m; 64 | } 65 | } 66 | 67 | typedef Message MessageFactory(Map map); 68 | 69 | class SystemMessageFactories { 70 | static Map messageFactories = { 71 | "log": (m) => new LogMessage.fromMap(m), 72 | "create": (m) => new CreateMessage.fromMap(m), 73 | "created": (m) => new CreatedMessage.fromMap(m), 74 | "clientId": (m) => new ClientIdMessage.fromMap(m), 75 | "open": (m) => new OpenMessage.fromMap(m), 76 | "close": (m) => new CloseMessage.fromMap(m), 77 | "snapshot": (m) => new SnapshotMessage.fromMap(m), 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /example/bin/demo_server.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | import 'dart:io'; 17 | 18 | import 'package:collab/server/server.dart'; 19 | import 'package:collab/text/text.dart'; 20 | 21 | void main(List argv) { 22 | String host = getHost(argv); 23 | host = (host == null) ? "127.0.0.1" : host; 24 | 25 | var collabServer = new CollabServer(); 26 | collabServer.registerDocumentType(new TextDocumentType()); 27 | StreamController sc = new StreamController(); 28 | sc.stream.transform(new WebSocketTransformer()).listen((WebSocket ws) { 29 | var connection = new WebSocketConnection(ws); 30 | collabServer.addConnection(connection); 31 | }); 32 | 33 | HttpServer.bind(host, 8080).then((HttpServer server) { 34 | server.listen((HttpRequest req) { 35 | if (req.uri.path == "/connect") { 36 | sc.add(req); 37 | } else { 38 | serveFile(req, req.response); 39 | } 40 | }); 41 | }); 42 | } 43 | 44 | String getHost(List argv) { 45 | for (int i = 0; i < argv.length; i++) { 46 | String a = argv[i]; 47 | if (a == "--host") { 48 | return argv[i+1]; 49 | } 50 | } 51 | return null; 52 | } 53 | 54 | Map contentTypes = const { 55 | "html": "text/html; charset=UTF-8", 56 | "dart": "application/dart", 57 | "js": "application/javascript", 58 | "css": "text/css", 59 | }; 60 | 61 | /// Very simple async static file server. Probably insecure! 62 | void serveFile(HttpRequest req, HttpResponse resp) { 63 | String path = req.uri.path.endsWith('/') 64 | ? ".${req.uri.path}index.html" 65 | : req.uri.path; 66 | var cwd = Directory.current.path; 67 | print("serving $path from $cwd"); 68 | 69 | File file = new File("$cwd/$path"); 70 | file.exists().then((exists) { 71 | if (exists) { 72 | file.readAsString().then((text) { 73 | if (text == null) { 74 | print("$path is empty?"); 75 | } 76 | resp.headers.set(HttpHeaders.CONTENT_TYPE, getContentType(file)); 77 | file.openRead().pipe(req.response).catchError((e) {}); 78 | }); 79 | } else { 80 | resp.statusCode = HttpStatus.NOT_FOUND; 81 | resp.close(); 82 | } 83 | }); 84 | } 85 | 86 | String getContentType(File file) => contentTypes[file.path.split('.').last]; 87 | -------------------------------------------------------------------------------- /lib/text/text.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library text; 16 | 17 | import 'package:collab/collab.dart'; 18 | import 'package:collab/utils.dart'; 19 | 20 | class TextDocumentType extends DocumentType { 21 | static final TextDocumentType _INSTANCE = new TextDocumentType(); 22 | 23 | String get id => "text"; 24 | 25 | TextDocument create(String id) { 26 | return new TextDocument(id); 27 | } 28 | 29 | Map get messageFactories => { 30 | "text": (m) => new TextOperation.fromMap(m) 31 | }; 32 | 33 | Map> get transforms => { 34 | "text" : { "text": (op1, op2) => _transformInsert(op1, op2) } 35 | }; 36 | 37 | static TextOperation _transformInsert(TextOperation op, TextOperation by) { 38 | int newPosition = (by.position < op.position) 39 | ? op.position + (by.inserted.length - by.deleted.length) 40 | : op.position; 41 | // should docVersion be updated? 42 | // should [by] have to have a sequence number? 43 | // A: yes, and it should be less than op.docVersion 44 | return new TextOperation(op.senderId, op.docId, op.docVersion, newPosition, 45 | op.deleted, op.inserted); 46 | } 47 | } 48 | 49 | /* 50 | * A simple text-based document. 51 | */ 52 | class TextDocument extends Document { 53 | String _content; 54 | 55 | TextDocument(String id) 56 | : this._content = "", 57 | super(id); 58 | 59 | TextDocument.fromString(String id, String this._content) 60 | : super(id); 61 | 62 | DocumentType get type => TextDocumentType._INSTANCE; 63 | 64 | void modify(int pos, String del, String ins) { 65 | if ((pos < 0) || (pos > _content.length)) { 66 | throw "illegal position: ${pos}, ${_content.length}"; 67 | } 68 | StringBuffer sb = new StringBuffer(); 69 | sb.write(_content.substring(0, pos)); 70 | sb.write(ins); 71 | sb.write(_content.substring(pos + del.length)); 72 | _content = sb.toString(); 73 | var event = new TextChangeEvent(this, pos, del, ins, _content); 74 | fireUpdate(event); 75 | } 76 | 77 | String serialize() => _content; 78 | void deserialize(String content) => modify(0, _content, content); 79 | } 80 | 81 | /* 82 | * Describes a change to a body of text. 83 | */ 84 | class TextChangeEvent extends DocumentChangeEvent { 85 | final int position; 86 | final String deleted; 87 | final String inserted; 88 | final String text; 89 | 90 | TextChangeEvent(Document document, this.position, this.deleted, this.inserted, 91 | this.text) 92 | : super(document); 93 | 94 | String toString() => 95 | "TextChangeEvent {$document.id, $position, $deleted, $inserted}"; 96 | } 97 | 98 | /* 99 | * Inserts a string into a text document. 100 | */ 101 | class TextOperation extends Operation { 102 | final int position; 103 | final String deleted; 104 | final String inserted; 105 | 106 | TextOperation(String senderId, String docId, int docVersion, this.position, 107 | this.deleted, this.inserted) 108 | : super("text", senderId, docId, docVersion); 109 | 110 | TextOperation.fromMap(Map map) 111 | : super.fromMap(map), 112 | position = map['position'], 113 | deleted = map['deleted'], 114 | inserted = map['inserted']; 115 | 116 | toMap([values]) => super.toMap(mergeMaps(values, { 117 | 'position': position, 'deleted': deleted, 'inserted': inserted})); 118 | 119 | void apply(TextDocument document) { 120 | document.modify(position, deleted, inserted); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/document.dart: -------------------------------------------------------------------------------- 1 | part of collab; 2 | 3 | // Copyright 2011 Google Inc. All Rights Reserved. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | 18 | abstract class DocumentType { 19 | String get id; 20 | Document create(String id); 21 | Map get messageFactories; 22 | Map> get transforms; 23 | } 24 | 25 | class DocumentChangeEvent { 26 | final Document document; 27 | 28 | DocumentChangeEvent(this.document); 29 | } 30 | 31 | typedef void DocumentChangeHandler(DocumentChangeEvent e); 32 | 33 | abstract class Document { 34 | final String id; 35 | int version; 36 | final List log; 37 | final List _handlers; 38 | 39 | Document(String this.id) 40 | : version = 0, 41 | log = new List(), 42 | _handlers = new List() { 43 | } 44 | 45 | DocumentType get type; 46 | 47 | void addChangeHandler(DocumentChangeHandler handler) { 48 | assert(handler != null); 49 | print("addChangeHandler"); 50 | _handlers.add(handler); 51 | } 52 | 53 | void fireUpdate(DocumentChangeEvent event) { 54 | print("fireUpdate"); 55 | _handlers.forEach((handler) { handler(event); }); 56 | } 57 | 58 | String serialize(); 59 | void deserialize(String data); 60 | } 61 | 62 | /* 63 | * Creates a [Document]. This is not an operation because it does 64 | * not operate on an existing document. 65 | */ 66 | class CreateMessage extends Message { 67 | final String docType; 68 | 69 | CreateMessage(this.docType, String senderId) : super("create", senderId); 70 | 71 | CreateMessage.fromMap(Map map) 72 | : super.fromMap(map), 73 | docType = map['docType']; 74 | 75 | toMap([values]) => super.toMap(mergeMaps(values, {'docType': docType})); 76 | } 77 | 78 | /* 79 | * Notifies a client that a document has been created. 80 | */ 81 | class CreatedMessage extends Message { 82 | String docId; 83 | String docType; 84 | CreatedMessage(this.docId, this.docType, [String replyTo]) 85 | : super("created", SERVER_ID, replyTo); 86 | 87 | CreatedMessage.fromMap(Map map) 88 | : super.fromMap(map), 89 | docId = map['docId'], 90 | docType = map['docType']; 91 | 92 | toMap([values]) => 93 | super.toMap(mergeMaps(values, {'docId': docId, 'docType': docType})); 94 | } 95 | 96 | /* 97 | * Tells the server that a client wants to listen to a document. 98 | */ 99 | class OpenMessage extends Message { 100 | final String docId; 101 | final String docType; 102 | 103 | OpenMessage(this.docId, this.docType, String senderId) 104 | : super("open", senderId); 105 | 106 | OpenMessage.fromMap(Map map) 107 | : super.fromMap(map), 108 | docId = map['docId'], 109 | docType = map['docType']; 110 | 111 | toMap([values]) => 112 | super.toMap(mergeMaps(values, {'docId': docId, 'docType': docType})); 113 | } 114 | 115 | /* 116 | * Tells the server that a client wants to stop listening to a document. 117 | */ 118 | class CloseMessage extends Message { 119 | final String docId; 120 | 121 | CloseMessage(String senderId, this.docId) : super("close", senderId); 122 | 123 | CloseMessage.fromMap(Map map) 124 | : super.fromMap(map), 125 | docId = map['docId']; 126 | 127 | toMap([values]) => super.toMap(mergeMaps(values, {'docId': docId})); 128 | } 129 | 130 | /* 131 | * Sends a snapshot of the current state of a document to a client. 132 | */ 133 | class SnapshotMessage extends Message { 134 | final String docId; 135 | final int version; 136 | final String content; 137 | 138 | SnapshotMessage(String senderId, this.docId, this.version, this.content) 139 | : super("snapshot", senderId); 140 | 141 | SnapshotMessage.fromMap(Map map) 142 | : super.fromMap(map), 143 | docId = map['docId'], 144 | version = map['version'], 145 | content = map['content']; 146 | 147 | toMap([values]) => super.toMap(mergeMaps(values, 148 | {'docId': docId, 'version': version, 'content': content})); 149 | } 150 | -------------------------------------------------------------------------------- /lib/client/web_utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library web_utils; 16 | 17 | import 'dart:html'; 18 | import 'dart:math'; 19 | import 'package:collab/collab.dart' as collab; 20 | import 'package:collab/text/text.dart' as text; 21 | import 'web_client.dart'; 22 | 23 | class TextChangeEvent { 24 | final Element target; 25 | final String text; 26 | final int position; 27 | final String deleted; 28 | final String inserted; 29 | 30 | TextChangeEvent(this.target, this.text, this.position, this.deleted, 31 | this.inserted); 32 | 33 | String toString() => "TextChangeEvent {text: $text, position: $position, " 34 | + "deleted: $deleted, inserted: $inserted}"; 35 | } 36 | 37 | typedef void TextChangeHandler(TextChangeEvent e); 38 | 39 | class TextChangeListener { 40 | final Element _element; 41 | final List _handlers; 42 | String _oldValue; 43 | 44 | TextChangeListener(this._element) 45 | : _handlers = new List() { 46 | _element.onKeyUp.listen((KeyboardEvent e) { 47 | int pos = (_element as dynamic).selectionStart; 48 | _onChange(); 49 | }); 50 | _element.onChange.listen((Event e) { 51 | int pos = (_element as dynamic).selectionStart; 52 | _onChange(); 53 | }); 54 | _oldValue = (_element as dynamic).value; 55 | } 56 | 57 | void addChangeHandler(TextChangeHandler handler) { 58 | _handlers.add(handler); 59 | } 60 | 61 | void reset() { 62 | _oldValue = (_element as dynamic).value; 63 | } 64 | 65 | /* 66 | * This algorithm works because there can only be one contiguous change as a 67 | * result of typing or pasting. If a paste contains a common substring with 68 | * the pasted over text, this will not attempt to find it and make more than 69 | * one delete/insert pair. This is actually good because it preserves user 70 | * intention when used in an OT system. 71 | */ 72 | void _onChange() { 73 | String newValue = (_element as dynamic).value; 74 | 75 | if (newValue == _oldValue) { 76 | return; 77 | } 78 | 79 | int start = 0; 80 | int end = 0; 81 | int oldLength = _oldValue.length; 82 | int newLength = newValue.length; 83 | 84 | while ((start < oldLength) && (start < newLength) 85 | && (_oldValue[start] == newValue[start])) { 86 | start++; 87 | } 88 | while ((start + end < oldLength) && (start + end < newLength) 89 | && (_oldValue[oldLength - end - 1] == newValue[newLength - end - 1])) { 90 | end++; 91 | } 92 | 93 | String deleted = _oldValue.substring(start, oldLength - end); 94 | String inserted = newValue.substring(start, newLength - end); 95 | _oldValue = newValue; 96 | _fire(newValue, start, deleted, inserted); 97 | } 98 | 99 | void _fire(String text, int position, String deleted, String inserted) { 100 | TextChangeEvent event = 101 | new TextChangeEvent(_element, text, position, deleted, inserted); 102 | _handlers.forEach((handler) { handler(event); }); 103 | } 104 | } 105 | 106 | void makeEditable(Element element, CollabWebClient client) { 107 | print("makeEditable"); 108 | TextChangeListener listener = new TextChangeListener(element); 109 | 110 | bool listen = true; 111 | listener.addChangeHandler((TextChangeEvent event) { 112 | print(event); 113 | if (listen) { 114 | listen = false; 115 | text.TextOperation op = new text.TextOperation(client.id, "test", 116 | client.docVersion, event.position, event.deleted, event.inserted); 117 | client.queue(op); 118 | listen = true; 119 | } 120 | }); 121 | 122 | client.document.addChangeHandler((collab.DocumentChangeEvent event) { 123 | if (listen && event is text.TextChangeEvent) { 124 | listen = false; 125 | int cursorPos = (element as dynamic).selectionStart; 126 | (element as dynamic).value = event.text; 127 | if (event.position < cursorPos) { 128 | cursorPos = 129 | max(0, cursorPos + event.inserted.length - event.deleted.length); 130 | } 131 | (element as dynamic).setSelectionRange(cursorPos, cursorPos); 132 | listener.reset(); 133 | listen = true; 134 | } 135 | }); 136 | } 137 | -------------------------------------------------------------------------------- /lib/client/web_client.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library web_client; 16 | 17 | import 'dart:async'; 18 | import 'dart:convert' show JSON; 19 | import 'dart:html' as html; 20 | import 'package:collab/collab.dart'; 21 | 22 | part 'connection.dart'; 23 | 24 | typedef void StatusHandler(int status); 25 | const int DISCONNECTED = 0; 26 | const int CONNECTED = 1; 27 | const int CONNECTING = 2; 28 | const int ERROR = 3; 29 | 30 | class CollabWebClient { 31 | String _clientId; 32 | Document _document; 33 | Map _messageFactories; 34 | Map> _transforms; 35 | Map _pendingRequests; // might not be necessary anymore 36 | List _statusHandlers; 37 | 38 | // Operations that have not been sent to the server yet 39 | List _queue; 40 | 41 | // The outstanding operation, if any. 42 | Operation _pending; 43 | 44 | // Operations received while the last sent operation is still pending. 45 | // These operations need to be transformed by the pending operation 46 | // if their sequence number is less than the pending operation. 47 | List _incoming; 48 | 49 | final Connection _connection; 50 | 51 | CollabWebClient(Connection this._connection, Document this._document) { 52 | _messageFactories = new Map.from(_document.type.messageFactories); 53 | _messageFactories.addAll(SystemMessageFactories.messageFactories); 54 | _transforms = new Map.from(_document.type.transforms); 55 | _pendingRequests = new Map(); 56 | _queue = new List(); 57 | _incoming = new List(); 58 | 59 | _statusHandlers = new List(); 60 | _onStatusChange(CONNECTING); 61 | 62 | _connection.stream.map(JSON.decode).listen((json) { 63 | var factory = _messageFactories[json['type']]; 64 | Message message = factory(json); 65 | _dispatch(message); 66 | }, 67 | onError: (error) { 68 | _onStatusChange(ERROR); 69 | print("error: $error"); 70 | }, 71 | onDone: () { 72 | _onStatusChange(DISCONNECTED); 73 | print("closed"); 74 | }); 75 | } 76 | 77 | Document get document => _document; 78 | 79 | String get id => _clientId; 80 | 81 | int get docVersion => _document.version; 82 | 83 | // TODO: change away from send, since only the client can send 84 | // might need a separate envelope from message 85 | void queue(Operation operation) { 86 | operation.apply(_document); 87 | if (_pending == null) { 88 | _pending = operation; 89 | send(operation); 90 | } else { 91 | _queue.add(operation); 92 | } 93 | } 94 | 95 | void send(Message message) { 96 | _connection.add(message.json); 97 | } 98 | 99 | void addStatusHandler(StatusHandler h) { 100 | _statusHandlers.add(h); 101 | } 102 | 103 | void _onStatusChange(int status) { 104 | _statusHandlers.forEach((h) { h(status); }); 105 | } 106 | 107 | void _dispatch(Message message) { 108 | if (message.type == "clientId") { 109 | _onClientId(message); 110 | } else if (message is Operation) { 111 | Operation op = message; 112 | if (op.senderId == _clientId) { 113 | // this should be the server transformed version of pending op with its 114 | // sequence number set. transform incoming ops by pending, since 115 | // pending was transformed by incoming on the server don't apply op. 116 | assert(op.id == _pending.id); 117 | List toRemove = []; 118 | _incoming.forEach((Operation i) { 119 | if (i.sequence < op.sequence) { 120 | var transform = _transforms[i.type][_pending.type]; 121 | var it = (transform == null) ? i : transform(i, _pending); 122 | _apply(it); 123 | toRemove.add(it); 124 | } 125 | }); 126 | toRemove.forEach((i) { 127 | _incoming.removeRange(_incoming.indexOf(i), 1); 128 | }); 129 | _pending.sequence = op.sequence; 130 | if (op.sequence > _document.version) { 131 | _document.version= op.sequence; 132 | } // else? 133 | _pending = null; 134 | if (!_queue.isEmpty) { 135 | _queue.forEach((o) { o.docVersion = op.sequence; }); 136 | var next = _queue.removeAt(0); 137 | _pending = next; 138 | send(next); 139 | } 140 | } else { 141 | // transform by pending? 142 | // transform queued ops? 143 | if (_pending != null) { 144 | _incoming.add(op); 145 | } else { 146 | _apply(op); 147 | } 148 | } 149 | } else if (message is SnapshotMessage) { 150 | _onSnapshot(message); 151 | } 152 | if (message.replyTo != null) { 153 | _onReply(message); 154 | } 155 | } 156 | 157 | void _apply(Operation op) { 158 | op.apply(_document); 159 | _document.log.add(op); 160 | if (op.sequence > _document.version) { 161 | _document.version = op.sequence; 162 | } 163 | } 164 | 165 | /** 166 | * Handles a reply message, calling the correct callback. 167 | */ 168 | void _onReply(Message response) { 169 | String replyTo = response.replyTo; 170 | if (replyTo != null) { 171 | Completer completer = _pendingRequests[replyTo]; 172 | if (completer == null) { 173 | print("unknown message replied to: $replyTo"); 174 | return; 175 | } 176 | _pendingRequests.remove(replyTo); 177 | completer.complete(response); 178 | } 179 | } 180 | 181 | void _onClientId(ClientIdMessage message) { 182 | _clientId = message.clientId; 183 | print("clientId: $_clientId"); 184 | // once we have a clientId, open a test doc 185 | OpenMessage om = 186 | new OpenMessage(_document.id, _document.type.id, _clientId); 187 | send(om); 188 | _onStatusChange(CONNECTED); 189 | } 190 | 191 | void _onSnapshot(SnapshotMessage message) { 192 | _document.deserialize(message.content); 193 | _document.version = message.version; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /lib/server/server.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library server; 16 | 17 | import 'dart:async'; 18 | import 'dart:collection'; 19 | import 'dart:io'; 20 | import 'dart:convert' show JSON; 21 | 22 | import 'package:collab/collab.dart'; 23 | import 'package:collab/utils.dart'; 24 | 25 | part 'connection.dart'; 26 | 27 | class CollabServer { 28 | // clientId -> connection 29 | final Map _connections; 30 | // docTypeId -> DocumentType 31 | final Map _docTypes; 32 | // messageType -> MessageFactory 33 | final Map _messageFactories; 34 | final Map> _transforms; 35 | // docId -> document 36 | final Map _documents; 37 | // docId -> clientId 38 | final Map> _listeners; 39 | final Queue _queue; 40 | 41 | CollabServer() 42 | : _connections = new Map(), 43 | _docTypes = new Map(), 44 | _messageFactories = new Map.from(SystemMessageFactories.messageFactories), 45 | _transforms = new Map>(), 46 | _documents = new Map(), 47 | _listeners = new Map>(), 48 | _queue = new Queue(); 49 | 50 | void addConnection(Connection connection) { 51 | String clientId = randomId(); 52 | _connections[clientId] = connection; 53 | connection.stream.map(JSON.decode).listen((json) { 54 | var factory = _messageFactories[json['type']]; 55 | var message = factory(json); 56 | _enqueue(message); 57 | }, 58 | onDone: () { 59 | print("closed: $clientId"); 60 | _removeConnection(clientId); 61 | }, 62 | onError: (e) { 63 | print("error: $clientId $e"); 64 | _removeConnection(clientId); 65 | }); 66 | ClientIdMessage message = new ClientIdMessage(SERVER_ID, clientId); 67 | connection.add(message.json); 68 | } 69 | 70 | void registerDocumentType(DocumentType docType) { 71 | _docTypes[docType.id] = docType; 72 | _messageFactories.addAll(docType.messageFactories); 73 | _transforms.addAll(docType.transforms); 74 | } 75 | 76 | void _enqueue(Message message) { 77 | _queue.add(message); 78 | _processDeferred(); 79 | } 80 | 81 | void _processDeferred() { 82 | Timer.run(() => _process()); 83 | } 84 | 85 | void _process() { 86 | if (!_queue.isEmpty) { 87 | _dispatch(_queue.removeFirst()); 88 | _processDeferred(); 89 | } 90 | } 91 | 92 | void _dispatch(Message message) { 93 | String clientId = message.senderId; 94 | print("dispatch: $message"); 95 | switch (message.type) { 96 | case "create": 97 | create(clientId, message); 98 | break; 99 | case "log": 100 | print((message as LogMessage).text); 101 | break; 102 | case "open": 103 | OpenMessage m = message; 104 | _open(clientId, m.docId, m.docType); 105 | break; 106 | case "close": 107 | CloseMessage m = message; 108 | _removeListener(clientId, m.docId); 109 | break; 110 | default: 111 | if (message is Operation) { 112 | _doOperation(message); 113 | } else { 114 | print("unknown message type: ${message.type}"); 115 | } 116 | } 117 | } 118 | 119 | void _doOperation(Operation op) { 120 | Document doc = _documents[op.docId]; 121 | // TODO: apply transform 122 | // transform by every applied op with a seq number greater than 123 | // op.docVersion those operations are in flight to the client that sent [op] 124 | // and will be transformed by op in the client. The result will be the same. 125 | Operation transformed = op; 126 | int currentVersion = doc.version; 127 | Queue newerOps = new Queue(); 128 | for (int i = doc.log.length - 1; i >= 0; i--) { 129 | Operation appliedOp = doc.log[i]; 130 | if (appliedOp.sequence > op.docVersion) { 131 | Transform t = _transforms[transformed.type][appliedOp.type]; 132 | transformed = (t == null) ? transformed : t(transformed, appliedOp); 133 | } 134 | } 135 | doc.version++; 136 | transformed.sequence = doc.version; 137 | transformed.apply(doc); 138 | doc.log.add(transformed); 139 | _broadcast(transformed); 140 | } 141 | 142 | void _broadcast(Operation op) { 143 | Set listenerIds = _listeners[op.docId]; 144 | if (listenerIds == null) { 145 | print("no listeners"); 146 | return; 147 | } 148 | for (String listenerId in listenerIds) { 149 | _send(listenerId, op); 150 | } 151 | } 152 | 153 | void _send(String clientId, Message message) { 154 | var connection = _connections[clientId]; 155 | if (connection == null) { 156 | // not sure why this happens sometimes 157 | _connections.remove(clientId); 158 | return; 159 | } 160 | connection.add(message.json); 161 | } 162 | 163 | void _open(String clientId, String docId, String docType) { 164 | if (_documents[docId] == null) { 165 | _create(docId, docType); 166 | } 167 | _addListener(clientId, docId); 168 | } 169 | 170 | void _addListener(String clientId, String docId) { 171 | _listeners.putIfAbsent(docId, () => new Set()); 172 | _listeners[docId].add(clientId); 173 | Document d = _documents[docId]; 174 | Message m = 175 | new SnapshotMessage(SERVER_ID, docId, d.version, d.serialize()); 176 | _send(clientId, m); 177 | } 178 | 179 | void _removeListener(String clientId, String docId) { 180 | _listeners.putIfAbsent(docId, () => new Set()); 181 | _listeners[docId].remove(clientId); 182 | } 183 | 184 | void create(String clientId, CreateMessage message) { 185 | var d = _create(randomId(), message.docType); 186 | CreatedMessage m = new CreatedMessage(d.id, d.type.id, message.id); 187 | _send(clientId, m); 188 | } 189 | 190 | Document _create(String docId, String docTypeId) { 191 | Document d = _docTypes[docTypeId].create(docId); 192 | _documents[d.id] = d; 193 | return d; 194 | } 195 | 196 | void _removeConnection(String clientId) { 197 | _connections.remove(clientId); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------