rave 0.1.2-java

Sign up to get free protection for your applications and to get access to all the features.
@@ -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