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.
@@ -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