Name: correlation/server 
1:
#!/usr/bin/ruby
2:
# Apache License, Version 2.0
3:
#
4:
# Copyright (c) 2013 Juergen Mangler
5:
#
6:
# Licensed under the Apache License, Version 2.0 (the "License");
7:
# you may not use this file except in compliance with the License.
8:
# You may obtain a copy of the License at
9:
#
10:
#     http://www.apache.org/licenses/LICENSE-2.0
11:
#
12:
# Unless required by applicable law or agreed to in writing, software
13:
# distributed under the License is distributed on an "AS IS" BASIS,
14:
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15:
# See the License for the specific language governing permissions and
16:
# limitations under the License.
17:
 
18:
require 'rubygems'
19:
require 'riddl/server'
20:
require 'json'
21:
require 'xml/smart'
22:
require 'fileutils'
23:
require 'term/ansicolor'
24:
class String
25:
  include Term::ANSIColor
26:
end
27:
 
28:
class Worker #{{{
29:
  def initialize(*para)
30:
    @para = para
31:
  end
32:
  def process(doc,mdoc,sdoc,f) #{{{
33:
    if doc.find("string(/g:gatewayRequest/g:header/g:operationName)") == mdoc.find("string(/g:gatewayResponse/g:header/g:operationName)")
34:
      ips = doc.find("/g:gatewayRequest/g:payload/g:inputParameter")
35:
      count = 0
36:
      ips.each do |e|
37:
        i = sdoc.find("/r:summary/r:info/r:info[@name=\"#{e.attributes['paramName']}\"]")
38:
        count += 1 if i.any? && i.first.attributes['value'] == e.text
39:
      end
40:
      count == ips.length ? File.basename(File.dirname(f)) : nil
41:
    end
42:
  end
43:
  private :process #}}}
44:
  def process_rule #{{{
45:
    doc = @para[0]
46:
    ret = []
47:
    Dir.glob(File.dirname(__FILE__) + "/messages/*/xml").sort{|a,b| a.split('/')[2].to_i <=> b.split('/')[2].to_i}.each do |f|
48:
      XML::Smart.open_unprotected(f) do |mdoc|
49:
        sdoc = nil
50:
        mdoc.register_namespace 'g', 'http://www.fp7-adventure.eu/xmlSchema/Gateways/'
51:
        mdoc.find("/g:gatewayResponse/g:payload/g:outputData[@paramName='summary']").each do |e|
52:
          if e.text_only?
53:
            sdoc = XML::Smart::string(e.text)
54:
            sdoc.root.namespaces.add(nil,'http://www.fp7-adventure.eu/xmlSchema/Gateways/')
55:
          else
56:
            sdoc = e.children.first.to_doc
57:
          end  
58:
          sdoc.register_namespace 'r', 'http://www.fp7-adventure.eu/xmlSchema/Gateways/'
59:
        end  
60:
        ret << process(doc,mdoc,sdoc,f)
61:
      end
62:
    end
63:
    ret.compact
64:
  end #}}}
65:
  def process_message #{{{
66:
    mdoc = @para[0]
67:
    sdoc = nil
68:
    mdoc.find("/g:gatewayResponse/g:payload/g:outputData[@paramName='summary']").each do |e|
69:
      if e.text_only?
70:
        sdoc = XML::Smart::string(e.text)
71:
        sdoc.root.namespaces.add(nil,'http://www.fp7-adventure.eu/xmlSchema/Gateways/')
72:
      else
73:
        sdoc = e.children.first.to_doc
74:
      end  
75:
      sdoc.register_namespace 'r', 'http://www.fp7-adventure.eu/xmlSchema/Gateways/'
76:
    end  
77:
    ret = []
78:
    Dir.glob(File.dirname(__FILE__) + "/rules/*/xml").each do |f|
79:
      XML::Smart.open_unprotected(f) do |doc|
80:
        doc.register_namespace 'g', 'http://www.fp7-adventure.eu/xmlSchema/Gateways/'
81:
        ret << process(doc,mdoc,sdoc,f)
82:
      end
83:
    end
84:
    ret.compact
85:
  end #}}}
86:
  def list #{{{
87:
    list = []
88:
    %w{rules messages}.each do |e|
89:
      Dir[File.dirname(__FILE__) + "/#{e}/*"].each do |f|
90:
        meta = {}
91:
        meta['id'] = File.basename(f)
92:
        meta['type'] = e.chop
93:
        meta.merge!(JSON.parse(File.read(f + '/meta'))) if File.exists?(f + '/meta')
94:
        list << meta
95:
      end if @para.empty? || @para[0] == e
96:
    end  
97:
    list.sort{|a,b| [a['type'],a['id'].to_i] <=> [b['type'],b['id'].to_i]}
98:
  end  #}}}
99:
  def details #{{{
100:
    return [] if @para.length < 2 && (@para[0] != 'rule' || @para[0] != 'message')
101:
    path = @para[0] == 'rule' ? 'rules' : 'messages'
102:
    name = File.dirname(__FILE__) + "/#{path}/#{@para[1].to_i}/xml"
103:
    File.exists?(name) ? File.read(name) : nil
104:
  end #}}}
105:
  def delete #{{{
106:
    if @para.length > 1 && (@para[0] == 'rule' || @para[0] == 'message')
107:
      path = @para.shift == 'rule' ? 'rules' : 'messages'
108:
      mess = nil
109:
      @para.each do |par|
110:
        name = File.dirname(__FILE__) + "/#{path}/#{par.to_i}"
111:
        if File.exists?(name)
112:
          FileUtils.rm_r(name)
113:
        else
114:
          mess = "some don't exist"
115:
        end
116:
      end
117:
      mess
118:
    else
119:
      "won't do"
120:
    end
121:
  end #}}}
122:
end #}}}
123:
 
124:
class RuleProcess < Riddl::Implementation #{{{
125:
  def response
126:
    rdoc = XML::Smart::string(@p[0].value.read)
127:
    rdoc.register_namespace 'g', 'http://www.fp7-adventure.eu/xmlSchema/Gateways/'
128:
    meta = {
129:
      'op' => rdoc.find('string(//g:header/g:operationName)'),
130:
      'time' => Time.now.to_s,
131:
      'source' => @h['CPEE_CALLBACK']
132:
    }
133:
 
134:
    save = true
135:
    messages = Worker.new(rdoc).process_rule
136:
    if messages.length > 0
137:
      what = JSON.parse(File.read(File.dirname(__FILE__) + "/messages/#{messages[0]}/meta"))
138:
      mdoc = XML::Smart.open_unprotected(File.dirname(__FILE__) + "/messages/#{messages[0]}/xml")
139:
      mdoc.register_namespace 'g', 'http://www.fp7-adventure.eu/xmlSchema/Gateways/'
140:
      client = Riddl::Client.interface("xmpp://" + meta['source'], "callback.xml", :xmpp => @env['xmpp'])
141:
      client.put Riddl::Parameter::Complex.new("gatewayResponse","text/xml",mdoc.to_s)
142:
      save = false if mdoc.find('string(//g:header/g:commandSequence)') == 'LAST'
143:
      Worker.new('message',messages[0]).delete if what['validity'] == 'FOREVER'
144:
    end
145:
 
146:
    if save
147:
      begin
148:
        id = id.nil? ? 0 : id + 1
149:
        Dir.mkdir(File.dirname(__FILE__) + "/rules/#{id}")
150:
      rescue SystemCallError
151:
        retry
152:
      end
153:
      
154:
      File.write(File.dirname(__FILE__) + "/rules/#{id}/meta", JSON.pretty_generate(meta))
155:
      File.write(File.dirname(__FILE__) + "/rules/#{id}/xml", rdoc.to_s)
156:
    end
157:
    nil
158:
  end
159:
end #}}}
160:
class MessageProcess < Riddl::Implementation #{{{
161:
  def response
162:
    mdoc = XML::Smart::string(@p[0].value.read)
163:
    mdoc.register_namespace 'g', 'http://www.fp7-adventure.eu/xmlSchema/Gateways/'
164:
    meta = {
165:
      'source' => @env['from'] ? @env['from'].split('/',2)[0] : 'some http',
166:
      'op' => mdoc.find('string(//g:header/g:operationName)'),
167:
      'validity' => (val = mdoc.find('string(//g:header/g:validity)')) == '' ? 'NOW' : val,
168:
      'time' => Time.now.to_s
169:
    }
170:
 
171:
    rules = Worker.new(mdoc).process_message
172:
    id = nil
173:
    if meta['validity'] == 'UNTIL'
174:
      Dir.glob(File.dirname(__FILE__) + "/messages/*/meta").each do |f|
175:
        tmeta = JSON.parse(File.read(f))
176:
        if tmeta['source'] == meta['source'] && tmeta['op'] == meta['op'] && tmeta['validity'] == meta['validity']
177:
          id = File.basename(File.dirname(f))
178:
        end  
179:
      end
180:
    end  
181:
    if (meta['validity'] == 'FOREVER' && rules.empty?) || (meta['validity'] == 'UNTIL' && id.nil?)
182:
      begin
183:
        id = id.nil? ? 0 : id + 1
184:
        Dir.mkdir(File.dirname(__FILE__) + "/messages/#{id}")
185:
      rescue SystemCallError
186:
        retry
187:
      end
188:
    end
189:
 
190:
    if id
191:
      File.write(File.dirname(__FILE__) + "/messages/#{id}/meta", JSON.pretty_generate(meta))
192:
      File.write(File.dirname(__FILE__) + "/messages/#{id}/xml", mdoc.to_s)
193:
    end
194:
    rules.each do |r|
195:
      to = JSON.parse(File.read(File.dirname(__FILE__) + "/rules/#{r}/meta"))
196:
      client = Riddl::Client.interface("xmpp://" + to['source'], "callback.xml", :xmpp => @env['xmpp'])
197:
      client.put Riddl::Parameter::Complex.new("gatewayResponse","text/xml",mdoc.to_s)
198:
      Worker.new('rule',r).delete if mdoc.find('string(//g:header/g:commandSequence)') == 'LAST'
199:
    end
200:
    nil
201:
  end
202:
end #}}}
203:
 
204:
class RuleList < Riddl::Implementation #{{{
205:
  def response
206:
    Riddl::Parameter::Complex.new("result","application/json",JSON::pretty_generate(Worker.new('rules').list))
207:
  end
208:
end #}}}
209:
class MessageList < Riddl::Implementation #{{{
210:
  def response
211:
    Riddl::Parameter::Complex.new("result","application/json",JSON::pretty_generate(Worker.new('messages').list))
212:
  end
213:
end #}}}
214:
class RuleDetails < Riddl::Implementation #{{{
215:
  def response
216:
    if (det = Worker.new('rule',@r[-1]).details).nil?
217:
      @status = 404
218:
    else  
219:
      Riddl::Parameter::Complex.new("gatewayRequest","text/xml",det)
220:
    end  
221:
  end
222:
end #}}}
223:
class MessageDetails < Riddl::Implementation #{{{
224:
  def response
225:
    if (det = Worker.new('message',@r[-1]).details).nil?
226:
      @status = 404
227:
    else  
228:
      Riddl::Parameter::Complex.new("gatewayResponse","text/xml",det)
229:
    end  
230:
  end
231:
end #}}}
232:
class RuleDelete < Riddl::Implementation #{{{
233:
  def response
234:
    @status = 404 if Worker.new('rule',@r[-1]).delete.nil?
235:
  end
236:
end #}}}
237:
class MessageDelete < Riddl::Implementation #{{{
238:
  def response
239:
    @status = 404 if Worker.new('message',@r[-1]).delete.nil?
240:
  end
241:
end #}}}
242:
 
243:
class Command < Riddl::Implementation
244:
  ECMDS = %w{list details delete correlate help}
245:
H_list = <<-end #{{{
246:
(rules|messages)
247:
  List all request and possible responses
248:
end
249:
#}}}
250:
H_help = <<-end #{{{
251:
 
252:
  Show this.
253:
end
254:
#}}}
255:
 H_details = <<-end #{{{
256:
(rule|message) ID
257:
  Send the contents of one of the listed request or response messages. If
258:
  second argument is omitted, details for request messages are shown.
259:
end
260:
#}}}
261:
H_delete = <<-end #{{{
262:
(rule|message) ID
263:
  Delete one of the request messages - this answers with an empty response
264:
  (LAST).
265:
  Example: delete rule 1
266:
end
267:
#}}}
268:
H_correlate = <<-end #{{{
269:
RULE-ID MESSAGE-ID
270:
  Manually correlate a rule to a message. While correlation is typically
271:
  automatic according to the parameters of the rule, this command can be used
272:
  to manually send back a message to execution.
273:
  The message is deleted according to normal NOW/UNTIL/FOREVER rules.
274:
  Example: correlate 1 2
275:
end
276:
#}}}
277:
 
278:
  def response
279:
    cmd = @p.shift.value
280:
    para = @p.map{|x|x.value}
281:
    res = case cmd
282:
      when 'commands'
283:
        ECMDS
284:
      when 'help'
285:
        ECMDS.flatten.map do |m|
286:
          h = self.class.const_defined?("H_#{m}".to_sym) ? self.class.const_get("H_#{m}".to_sym) : "\n"
287:
          "#{m.to_s.red.bold} #{h}"
288:
        end.join
289:
      when 'correlate'  
290:
        n1 = File.dirname(__FILE__) + "/rules/#{para[0].to_i}/meta"
291:
        n2 = File.dirname(__FILE__) + "/messages/#{para[1].to_i}/xml"
292:
        if para.length == 2 && File.exists?(n1) && File.exists?(n2)
293:
          to = JSON.parse(File.read(n1))
294:
          data = File.read(n2)
295:
          client = Riddl::Client.interface("xmpp://" + to['source'], "callback.xml", :xmpp => @env['xmpp'])
296:
          client.put Riddl::Parameter::Complex.new("gatewayResponse","text/xml",data)
297:
          doc = XML::Smart.string(data)
298:
          doc.register_namespace 'g', 'http://www.fp7-adventure.eu/xmlSchema/Gateways/'
299:
          Worker.new('rule',para[0]).delete if doc.find('string(//g:header/g:commandSequence)') == 'LAST'
300:
          Worker.new('message',para[1]).delete if ['NOW','FOREVER'].include?(doc.find('string(//g:header/g:validity)'))
301:
        end  
302:
        nil
303:
      when 'list'
304:
        Worker.new(*para).list
305:
      when 'details'
306:
        Worker.new(*para).details
307:
      when 'delete'
308:
        Worker.new(*para).delete
309:
      else
310:
        "that's not a valid command"
311:
    end || []
312:
    res = [res] unless res.is_a?(Hash) || res.is_a?(Array)
313:
    Riddl::Parameter::Complex.new("result","application/json",JSON::generate(res))
314:
  end
315:
end
316:
 
317:
Riddl::Server.new(File.dirname(__FILE__) + '/correlation.xml', :port => 9301) do
318:
  xmpp '[email protected]', 'adventure_correlation'
319:
  accessible_description true
320:
  cross_site_xhr true
321:
 
322:
  on resource do
323:
    run RuleProcess if post 'gateway-request'
324:
    run MessageProcess if put 'gateway-response'
325:
    run Command if put 'command'
326:
    on resource 'rules' do
327:
      run RuleList if get
328:
      on resource do
329:
        run RuleDetails if get
330:
        run RuleDelete if delete
331:
      end
332:
    end
333:
    on resource 'messages' do
334:
      run MessageList if get
335:
      on resource do
336:
        run MessageDetails if get
337:
        run MessageDelete if delete
338:
      end
339:
    end
340:
  end
341:
end.loop!