rave 0.1.2-java
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/bin/rave +28 -0
- data/lib/commands/appcfg.rb +9 -0
- data/lib/commands/create.rb +153 -0
- data/lib/commands/server.rb +8 -0
- data/lib/commands/task.rb +156 -0
- data/lib/commands/usage.rb +19 -0
- data/lib/commands/war.rb +28 -0
- data/lib/exceptions.rb +20 -0
- data/lib/ext/logger.rb +7 -0
- data/lib/gems.yaml +9 -0
- data/lib/jars/appengine-api-1.0-sdk-1.3.0.jar +0 -0
- data/lib/mixins/controller.rb +72 -0
- data/lib/mixins/data_format.rb +206 -0
- data/lib/mixins/logger.rb +19 -0
- data/lib/mixins/object_factory.rb +87 -0
- data/lib/mixins/time_utils.rb +19 -0
- data/lib/models/annotation.rb +148 -0
- data/lib/models/blip.rb +305 -0
- data/lib/models/component.rb +42 -0
- data/lib/models/context.rb +174 -0
- data/lib/models/document.rb +9 -0
- data/lib/models/element.rb +113 -0
- data/lib/models/event.rb +230 -0
- data/lib/models/operation.rb +79 -0
- data/lib/models/range.rb +14 -0
- data/lib/models/robot.rb +79 -0
- data/lib/models/user.rb +62 -0
- data/lib/models/wave.rb +45 -0
- data/lib/models/wavelet.rb +269 -0
- data/lib/ops/blip_ops.rb +233 -0
- data/lib/rave.rb +28 -0
- metadata +135 -0
Binary file
|
@@ -0,0 +1,72 @@
|
|
1
|
+
#Contains the Rack #call method - to be mixed in to the Robot class
|
2
|
+
module Rave
|
3
|
+
module Mixins
|
4
|
+
module Controller
|
5
|
+
include Logger
|
6
|
+
|
7
|
+
def call(env)
|
8
|
+
request = Rack::Request.new(env)
|
9
|
+
path = request.path_info
|
10
|
+
method = request.request_method
|
11
|
+
logger.info("#{method}ing #{path}")
|
12
|
+
begin
|
13
|
+
#There are only 3 URLs that Wave can access:
|
14
|
+
# robot capabilities, robot profile, and event notification
|
15
|
+
if path == "/_wave/capabilities.xml" && method == "GET"
|
16
|
+
[ 200, { 'Content-Type' => 'text/xml' }, capabilities_xml ]
|
17
|
+
elsif path == "/_wave/robot/profile" && method == "GET"
|
18
|
+
[ 200, { 'Content-Type' => 'application/json' }, profile_json ]
|
19
|
+
elsif path == "/_wave/robot/jsonrpc" && method == "POST"
|
20
|
+
body = request.body.read
|
21
|
+
context, events = parse_json_body(body)
|
22
|
+
events.each do |event|
|
23
|
+
handle_event(event, context)
|
24
|
+
end
|
25
|
+
response = context.to_json
|
26
|
+
logger.info("Structure (after):\n#{context.print_structure}")
|
27
|
+
logger.info("Response:\n#{response}")
|
28
|
+
[ 200, { 'Content-Type' => 'application/json' }, response ]
|
29
|
+
elsif cron_job = @cron_jobs.find { |job| job[:path] == path }
|
30
|
+
body = request.body.read
|
31
|
+
context, events = parse_json_body(body)
|
32
|
+
self.send(cron_job[:handler], context)
|
33
|
+
[ 200, { 'Content-Type' => 'application/json' }, context.to_json ]
|
34
|
+
elsif File.exist?(file = File.join(".", "public", *(path.split("/"))))
|
35
|
+
#Static resource
|
36
|
+
[ 200, { 'Content-Type' => static_resource_content_type(file) }, File.open(file) { |f| f.read } ]
|
37
|
+
elsif self.respond_to?(:custom_routes)
|
38
|
+
#Let the custom route method defined in the robot take care of the call
|
39
|
+
self.custom_routes(request, path, method)
|
40
|
+
else
|
41
|
+
logger.warning("404 - Not Found: #{path}")
|
42
|
+
[ 404, { 'Content-Type' => 'text/html' }, "404 - Not Found" ]
|
43
|
+
end
|
44
|
+
rescue Exception => e
|
45
|
+
logger.warning("500 - Internal Server Error: #{path}")
|
46
|
+
logger.warning("#{e.class}: #{e.message}\n\n#{e.backtrace.join("\n")}")
|
47
|
+
[ 500, { 'Content-Type' => 'text/html' }, "500 - Internal Server Error"]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
protected
|
52
|
+
def static_resource_content_type(path)
|
53
|
+
case (ext = File.extname(path))
|
54
|
+
when '.html', '.htm'
|
55
|
+
'text/html'
|
56
|
+
when '.xml'
|
57
|
+
'text/xml'
|
58
|
+
when '.gif'
|
59
|
+
'image/gif'
|
60
|
+
when '.jpeg', '.jpg'
|
61
|
+
'image/jpeg'
|
62
|
+
when '.tif', '.tiff'
|
63
|
+
'image/tiff'
|
64
|
+
when '.txt', ''
|
65
|
+
'text/plain'
|
66
|
+
else
|
67
|
+
"application/#{ext}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
#This mixin provides methods for robots to deal with parsing and presenting JSON and XML
|
2
|
+
module Rave
|
3
|
+
module Mixins
|
4
|
+
module DataFormat
|
5
|
+
include Logger
|
6
|
+
|
7
|
+
PROFILE_JAVA_CLASS = 'com.google.wave.api.ParticipantProfile'
|
8
|
+
|
9
|
+
#Returns this robot's capabilities in XML
|
10
|
+
def capabilities_xml
|
11
|
+
xml = Builder::XmlMarkup.new
|
12
|
+
xml.instruct!
|
13
|
+
xml.tag!("w:robot", "xmlns:w" => "http://wave.google.com/extensions/robots/1.0") do
|
14
|
+
xml.tag!("w:version", @version)
|
15
|
+
xml.tag!("w:capabilities") do
|
16
|
+
@handlers.keys.each do |capability|
|
17
|
+
xml.tag!("w:capability", "name" => capability)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
unless @cron_jobs.empty?
|
21
|
+
xml.tag!("w:crons") do
|
22
|
+
@cron_jobs.each do |job|
|
23
|
+
xml.tag!("w:cron", "path" => job[:path], "timerinseconds" => job[:seconds])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
attrs = { "name" => @name }
|
28
|
+
attrs["imageurl"] = @image_url if @image_url
|
29
|
+
attrs["profileurl"] = @profile_url if @profile_url
|
30
|
+
xml.tag!("w:profile", attrs)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
#Returns the robot's profile in json format
|
35
|
+
def profile_json
|
36
|
+
{
|
37
|
+
'name' => @name,
|
38
|
+
'imageUrl' => @image_url,
|
39
|
+
'profileUrl' => @profile_url,
|
40
|
+
'javaClass' => PROFILE_JAVA_CLASS,
|
41
|
+
}.to_json.gsub('\/','/')
|
42
|
+
end
|
43
|
+
|
44
|
+
#Parses context and event info from JSON input
|
45
|
+
def parse_json_body(json)
|
46
|
+
logger.info("Received:\n#{json.to_s}")
|
47
|
+
data = JSON.parse(json)
|
48
|
+
#Create Context
|
49
|
+
context = context_from_json(data)
|
50
|
+
#Create events
|
51
|
+
events = events_from_json(data, context)
|
52
|
+
logger.info("Structure (before):\n#{context.print_structure}")
|
53
|
+
logger.info("Events: #{events.map { |e| e.type }.join(', ')}")
|
54
|
+
return context, events
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
def context_from_json(json)
|
59
|
+
blips = {}
|
60
|
+
blips_from_json(json).each do |blip|
|
61
|
+
blips[blip.id] = blip
|
62
|
+
end
|
63
|
+
wavelets = {}
|
64
|
+
wavelets_from_json(json).each do |wavelet|
|
65
|
+
wavelets[wavelet.id] = wavelet
|
66
|
+
end
|
67
|
+
waves = {}
|
68
|
+
#Waves aren't sent back, but we can reconstruct them from the wavelets
|
69
|
+
waves_from_wavelets(wavelets).each do |wave|
|
70
|
+
waves[wave.id] = wave
|
71
|
+
end
|
72
|
+
Rave::Models::Context.new(
|
73
|
+
:waves => waves,
|
74
|
+
:wavelets => wavelets,
|
75
|
+
:blips => blips,
|
76
|
+
:robot => self
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
def blips_from_json(json)
|
81
|
+
map_to_hash(json['blips']).values.collect do |blip_data|
|
82
|
+
Rave::Models::Blip.new(
|
83
|
+
:id => blip_data['blipId'],
|
84
|
+
:annotations => annotations_from_json(blip_data),
|
85
|
+
:child_blip_ids => list_to_array(blip_data['childBlipIds']),
|
86
|
+
:content => blip_data['content'],
|
87
|
+
:contributors => list_to_array(blip_data['contributors']),
|
88
|
+
:creator => blip_data['creator'],
|
89
|
+
:elements => elements_from_json(blip_data['elements']),
|
90
|
+
:last_modified_time => blip_data['lastModifiedTime'],
|
91
|
+
:parent_blip_id => blip_data['parentBlipId'],
|
92
|
+
:version => blip_data['version'],
|
93
|
+
:wave_id => blip_data['waveId'],
|
94
|
+
:wavelet_id => blip_data['waveletId']
|
95
|
+
)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def elements_from_json(elements_map)
|
100
|
+
elements = {}
|
101
|
+
|
102
|
+
map_to_hash(elements_map).each_pair do |position, data|
|
103
|
+
elements[position.to_i] = Element.create(data['type'], map_to_hash(data['properties']))
|
104
|
+
end
|
105
|
+
|
106
|
+
elements
|
107
|
+
end
|
108
|
+
|
109
|
+
# Convert a json-java list (which may not be defined) into an array.
|
110
|
+
# Defaults to an empty array.
|
111
|
+
def list_to_array(list)
|
112
|
+
if list.nil?
|
113
|
+
[]
|
114
|
+
else
|
115
|
+
list['list'] || []
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Convert a json-java map (which may not be defined) into a hash. Defaults
|
120
|
+
# to an empty hash.
|
121
|
+
def map_to_hash(map)
|
122
|
+
if map.nil?
|
123
|
+
{}
|
124
|
+
else
|
125
|
+
map['map'] || {}
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def annotations_from_json(json)
|
130
|
+
list_to_array(json['annotation']).collect do |annotation|
|
131
|
+
Rave::Models::Annotation.create(
|
132
|
+
annotation['name'],
|
133
|
+
annotation['value'],
|
134
|
+
range_from_json(annotation['range'])
|
135
|
+
)
|
136
|
+
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def range_from_json(json)
|
141
|
+
Range.new(json['start'], json['end'])
|
142
|
+
end
|
143
|
+
|
144
|
+
def events_from_json(json, context)
|
145
|
+
list_to_array(json['events']).collect do |event|
|
146
|
+
properties = {}
|
147
|
+
event['properties']['map'].each do |key, value|
|
148
|
+
properties[key] = case value
|
149
|
+
when String # Just a string, as in blipId.
|
150
|
+
value
|
151
|
+
when Hash # Serialised array, such as in participantsAdded.
|
152
|
+
value['list']
|
153
|
+
else
|
154
|
+
raise "Unrecognised property #{value} #{value.class}"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
Rave::Models::Event.create(event['type'],
|
158
|
+
:timestamp => event['timestamp'],
|
159
|
+
:modified_by => event['modifiedBy'],
|
160
|
+
:properties => properties,
|
161
|
+
:context => context,
|
162
|
+
:robot => self
|
163
|
+
)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def wavelets_from_json(json)
|
168
|
+
#Currently only one wavelet is sent back
|
169
|
+
#TODO: should this look at the wavelet's children too?
|
170
|
+
wavelet = json['wavelet']
|
171
|
+
if wavelet
|
172
|
+
[
|
173
|
+
Rave::Models::Wavelet.new(
|
174
|
+
:creator => wavelet['creator'],
|
175
|
+
:creation_time => wavelet['creationTime'],
|
176
|
+
:data_documents => map_to_hash(wavelet['dataDocuments']),
|
177
|
+
:last_modifed_time => wavelet['lastModifiedTime'],
|
178
|
+
:participants => list_to_array(wavelet['participants']),
|
179
|
+
:root_blip_id => wavelet['rootBlipId'],
|
180
|
+
:title => wavelet['title'],
|
181
|
+
:version => wavelet['version'],
|
182
|
+
:wave_id => wavelet['waveId'],
|
183
|
+
:id => wavelet['waveletId']
|
184
|
+
)
|
185
|
+
]
|
186
|
+
else
|
187
|
+
[]
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def waves_from_wavelets(wavelets)
|
192
|
+
wave_wavelet_map = {}
|
193
|
+
if wavelets
|
194
|
+
wavelets.values.each do |wavelet|
|
195
|
+
wave_wavelet_map[wavelet.wave_id] ||= []
|
196
|
+
wave_wavelet_map[wavelet.wave_id] << wavelet.id
|
197
|
+
end
|
198
|
+
end
|
199
|
+
wave_wavelet_map.collect do |wave_id, wavelet_ids|
|
200
|
+
Rave::Models::Wave.new(:id => wave_id, :wavelet_ids => wavelet_ids)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Rave
|
2
|
+
module Mixins
|
3
|
+
module Logger
|
4
|
+
|
5
|
+
def logger
|
6
|
+
if @logger.nil?
|
7
|
+
if RUBY_PLATFORM == 'java'
|
8
|
+
@logger = java.util.logging.Logger.getLogger(self.class.to_s)
|
9
|
+
else
|
10
|
+
#TODO: Need to be able to configure output
|
11
|
+
@logger = ::Logger.new(STDOUT)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
@logger
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Rave
|
2
|
+
module Mixins
|
3
|
+
# Abstract object that allows you to create instances of the classes inside
|
4
|
+
# it based on providing a type name.
|
5
|
+
module ObjectFactory
|
6
|
+
WILDCARD = '*' unless defined? WILDCARD
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
base.class_eval do
|
10
|
+
# Store the registered classes in a class instance variable.
|
11
|
+
class << self
|
12
|
+
attr_reader :class_by_type_mapping
|
13
|
+
attr_reader :class_by_pattern_mapping
|
14
|
+
end
|
15
|
+
|
16
|
+
@class_by_type_mapping = {}
|
17
|
+
@class_by_pattern_mapping = {}
|
18
|
+
|
19
|
+
class_eval(<<-END, __FILE__, __LINE__)
|
20
|
+
def self.classes_by_type
|
21
|
+
::#{self.name}.class_by_type_mapping
|
22
|
+
end
|
23
|
+
def self.classes_by_pattern
|
24
|
+
::#{self.name}.class_by_pattern_mapping
|
25
|
+
end
|
26
|
+
END
|
27
|
+
|
28
|
+
# Object factory method.
|
29
|
+
#
|
30
|
+
# :type - Type of object to create [String]
|
31
|
+
def self.create(type, *args, &block)
|
32
|
+
if classes_by_type.has_key? type
|
33
|
+
return classes_by_type[type].new(*args, &block)
|
34
|
+
elsif
|
35
|
+
# Check for pattern-based types. Check for longer matches before shorter ones.
|
36
|
+
patterns = classes_by_pattern.keys.sort { |a, b| b.to_s.length <=> a.to_s.length }
|
37
|
+
patterns.each do |pattern|
|
38
|
+
if type =~ pattern
|
39
|
+
return classes_by_pattern[pattern].new($1, *args, &block)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
raise ArgumentError.new("Unknown #{self} type #{type}")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Is this type able to be created?
|
47
|
+
def self.valid_type?(type)
|
48
|
+
classes_by_type.has_key? type
|
49
|
+
end
|
50
|
+
|
51
|
+
# Register this class with its factory.
|
52
|
+
def self.factory_register(type)
|
53
|
+
classes_by_type[type] = self
|
54
|
+
|
55
|
+
# * in a type indicates a wildcard.
|
56
|
+
if type[WILDCARD]
|
57
|
+
classes_by_pattern[/^#{type.sub(WILDCARD, '(.*)')}$/] = self
|
58
|
+
end
|
59
|
+
|
60
|
+
class << self
|
61
|
+
def type; @type.dup; end
|
62
|
+
end
|
63
|
+
|
64
|
+
@type = type
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
# Classes that can be generated by the factory [Array of Class]
|
69
|
+
def self.classes
|
70
|
+
classes_by_type.values
|
71
|
+
end
|
72
|
+
|
73
|
+
# Types that can be generated by the factory [Array of String]
|
74
|
+
def self.types
|
75
|
+
classes_by_type.keys
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Type name for this class [String]
|
81
|
+
attr_reader :type
|
82
|
+
def type # :nodoc:
|
83
|
+
self.class.type
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Rave
|
2
|
+
module Mixins
|
3
|
+
module TimeUtils
|
4
|
+
|
5
|
+
def time_from_json(time)
|
6
|
+
if time
|
7
|
+
time_s = time.to_s
|
8
|
+
epoch = if time_s.length > 10
|
9
|
+
"#{time_s[0, 10]}.#{time_s[10..-1]}".to_f
|
10
|
+
else
|
11
|
+
time.to_i
|
12
|
+
end
|
13
|
+
Time.at(epoch)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'mixins/object_factory'
|
2
|
+
|
3
|
+
module Rave
|
4
|
+
module Models
|
5
|
+
# An annotation applying styling or other meta-data to a section of text.
|
6
|
+
class Annotation
|
7
|
+
include Rave::Mixins::ObjectFactory
|
8
|
+
|
9
|
+
JAVA_CLASS = "com.google.wave.api.Annotation"
|
10
|
+
|
11
|
+
# Name of the annotation type [String]
|
12
|
+
def name # :nodoc:
|
13
|
+
# If @id is defined, then put that into the type, otherwise just the type is fine.
|
14
|
+
@id ? type.sub(WILDCARD, @id) : type
|
15
|
+
end
|
16
|
+
|
17
|
+
# Value of the annotation [String]
|
18
|
+
def value # :nodoc:
|
19
|
+
@value.dup
|
20
|
+
end
|
21
|
+
|
22
|
+
# Range of characters over which the annotation applies [Range]
|
23
|
+
def range # :nodoc:
|
24
|
+
@range.dup
|
25
|
+
end
|
26
|
+
|
27
|
+
# +value+:: Value of the annotation [String]
|
28
|
+
# +range+:: Range of characters that the annotation applies to [Range]
|
29
|
+
def initialize(value, range); end
|
30
|
+
# +id+:: The non-class-dependent part of the name [String]
|
31
|
+
# +value+:: Value of the annotation [String]
|
32
|
+
# +range+:: Range of characters that the annotation applies to [Range]
|
33
|
+
def initialize(id, value, range); end
|
34
|
+
def initialize(*args) # :nodoc:
|
35
|
+
case args.length
|
36
|
+
when 3
|
37
|
+
@id, @value, @range = args
|
38
|
+
when 2
|
39
|
+
@value, @range = args
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_json # :nodoc:
|
44
|
+
{
|
45
|
+
'javaClass' => JAVA_CLASS,
|
46
|
+
'name' => name,
|
47
|
+
'value' => value,
|
48
|
+
'range' => range,
|
49
|
+
}.to_json
|
50
|
+
end
|
51
|
+
|
52
|
+
factory_register '*' # Accept all unrecognised annotations.
|
53
|
+
|
54
|
+
# Annotation classes:
|
55
|
+
|
56
|
+
# Language selected, such as "en", "de", etc.
|
57
|
+
class Language < Annotation
|
58
|
+
factory_register 'lang'
|
59
|
+
end
|
60
|
+
|
61
|
+
# Style, acting the same as the similarly named CSS properties.
|
62
|
+
class Style < Annotation
|
63
|
+
|
64
|
+
factory_register 'style/*' # Accept all unrecognised style annotations.
|
65
|
+
|
66
|
+
class BackgroundColor < Style
|
67
|
+
factory_register 'style/backgroundColor'
|
68
|
+
end
|
69
|
+
|
70
|
+
class Color < Style
|
71
|
+
factory_register 'style/color'
|
72
|
+
end
|
73
|
+
|
74
|
+
class FontFamily < Style
|
75
|
+
factory_register 'style/fontFamily'
|
76
|
+
end
|
77
|
+
|
78
|
+
class FontSize < Style
|
79
|
+
factory_register 'style/fontSize'
|
80
|
+
end
|
81
|
+
|
82
|
+
class FontWeight < Style
|
83
|
+
factory_register 'style/fontWeight'
|
84
|
+
end
|
85
|
+
|
86
|
+
class TextDecoration < Style
|
87
|
+
factory_register 'style/textDecoration'
|
88
|
+
end
|
89
|
+
|
90
|
+
class VerticalAlign < Style
|
91
|
+
factory_register 'style/verticalAlign'
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class Conversation < Annotation
|
96
|
+
factory_register 'conv/*' # Accept all unrecognised conv annotations.
|
97
|
+
|
98
|
+
class Title < Conversation
|
99
|
+
factory_register "conv/title"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# (Abstract)
|
104
|
+
class Link < Annotation
|
105
|
+
factory_register 'link/*' # Accept all unrecognised link annotations.
|
106
|
+
|
107
|
+
class Manual < Link
|
108
|
+
factory_register "link/manual"
|
109
|
+
end
|
110
|
+
|
111
|
+
class Auto < Link
|
112
|
+
factory_register "link/autoA"
|
113
|
+
end
|
114
|
+
|
115
|
+
class Wave < Link
|
116
|
+
factory_register "link/waveA"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# (Abstract)
|
121
|
+
class User < Annotation
|
122
|
+
factory_register 'user/*' # Accept all unrecognised user annotations.
|
123
|
+
|
124
|
+
# Session ID for the user annotation.
|
125
|
+
def session_id # :nodoc:
|
126
|
+
name =~ %r!/([^/]+)$!
|
127
|
+
$1
|
128
|
+
end
|
129
|
+
|
130
|
+
def initialize(session_id, value, range)
|
131
|
+
super
|
132
|
+
end
|
133
|
+
|
134
|
+
class Document < User
|
135
|
+
factory_register "user/d/*"
|
136
|
+
end
|
137
|
+
|
138
|
+
class Selection < User
|
139
|
+
factory_register "user/r/*"
|
140
|
+
end
|
141
|
+
|
142
|
+
class Focus < User
|
143
|
+
factory_register "user/e/*"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|