├── .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 |
--------------------------------------------------------------------------------