nitro 0.22.0 → 0.23.0

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.
Files changed (54) hide show
  1. data/CHANGELOG +109 -1250
  2. data/INSTALL +3 -2
  3. data/README +5 -4
  4. data/Rakefile +1 -1
  5. data/bin/nitrogen +1 -1
  6. data/doc/AUTHORS +20 -6
  7. data/doc/CHANGELOG.3 +1314 -0
  8. data/doc/RELEASES +90 -0
  9. data/lib/nitro.rb +6 -17
  10. data/lib/nitro/adapter/cgi.rb +11 -0
  11. data/lib/nitro/adapter/webrick.rb +7 -1
  12. data/lib/nitro/caching/stores.rb +4 -6
  13. data/lib/nitro/compiler.rb +1 -1
  14. data/lib/nitro/compiler/errors.rb +1 -1
  15. data/lib/nitro/context.rb +11 -4
  16. data/lib/nitro/controller.rb +0 -3
  17. data/lib/nitro/cookie.rb +4 -3
  18. data/lib/nitro/dispatcher.rb +7 -1
  19. data/lib/nitro/dispatcher/nice.rb +6 -1
  20. data/lib/nitro/element.rb +2 -2
  21. data/lib/nitro/mixin/benchmark.rb +16 -0
  22. data/lib/nitro/mixin/form.rb +171 -55
  23. data/lib/nitro/mixin/rss.rb +1 -1
  24. data/lib/nitro/mixin/xhtml.rb +91 -4
  25. data/lib/nitro/render.rb +25 -6
  26. data/lib/nitro/request.rb +13 -3
  27. data/lib/nitro/scaffold.rb +91 -68
  28. data/lib/nitro/scaffold/relations.rb +54 -0
  29. data/lib/nitro/server.rb +8 -5
  30. data/lib/nitro/server/runner.rb +4 -3
  31. data/lib/nitro/service.rb +3 -1
  32. data/lib/nitro/service/xmlrpc.rb +1 -1
  33. data/lib/nitro/session.rb +69 -51
  34. data/lib/nitro/session/drb.rb +5 -7
  35. data/lib/nitro/session/drbserver.rb +4 -6
  36. data/lib/nitro/session/memory.rb +4 -6
  37. data/lib/part/admin.rb +6 -0
  38. data/lib/part/admin/controller.rb +24 -0
  39. data/lib/part/admin/skin.rb +21 -0
  40. data/lib/part/admin/template/index.xhtml +9 -0
  41. data/proto/public/js/cookies.js +122 -0
  42. data/proto/public/scaffold/edit.xhtml +4 -0
  43. data/proto/public/scaffold/form.xhtml +7 -0
  44. data/proto/public/scaffold/list.xhtml +15 -0
  45. data/proto/public/scaffold/new.xhtml +4 -0
  46. data/proto/public/scaffold/view.xhtml +0 -0
  47. data/proto/script/benchmark +19 -0
  48. data/test/nitro/adapter/tc_cgi.rb +32 -2
  49. data/test/nitro/mixin/tc_xhtml.rb +6 -0
  50. data/test/nitro/tc_dispatcher.rb +0 -17
  51. data/test/nitro/tc_render.rb +58 -0
  52. data/test/nitro/tc_server.rb +2 -0
  53. data/test/nitro/tc_session.rb +16 -0
  54. metadata +104 -85
@@ -1,3 +1,93 @@
1
+ == Version 0.23.0
2
+
3
+ The summer vacations are over and there is a brand new Nitro
4
+ release. There is a preview of the new Scaffolder (also handles
5
+ Og relations), support for Tagging (folksonomy), lots of small
6
+ features and improvements and many bug fixes. Additionally, the
7
+ code has been restructured to utilize the excellent Nano and Mega
8
+ support libraries.
9
+
10
+ Most notable additions:
11
+
12
+ * Scaffolding reloaded. The scaffolding infrastructure is
13
+ reimplemented to generate more flexible code. The automatically
14
+ generated forms allow for visualization and editing of
15
+ Og relations such as HasMany and BelongsTo.
16
+
17
+ For example when rendering a BelongsTo relation all possible
18
+ parents are presented with a select element. When rendering a
19
+ HasMany relation a list of all children is presented.
20
+
21
+ Moreover, an experimental admin component is provided. Just add the
22
+ line:
23
+
24
+ require 'part/admin'
25
+
26
+ and surf
27
+
28
+ http://www.mysite.com/admin
29
+
30
+ To enter a simple administration screen.
31
+
32
+ WARNING: This feature is considered a preview and will be
33
+ improved in a future version.
34
+
35
+ * Major cleanup in the Glue subproject. Some files are moved
36
+ to the nano/mega project. Nano/Mega is now used throughout
37
+ Nitro and really makes development so much easier.
38
+
39
+ * Introduced Og Taggable mixin. It was never easier to add
40
+ tagging to your application.
41
+
42
+ class Article
43
+ include Og::Taggable
44
+ ..
45
+ end
46
+
47
+ article.tag('navel', 'gmosx', 'nitro')
48
+ article.tags
49
+ article.tag_names
50
+ Article.find_with_tags('navel', 'gmosx')
51
+ Article.find_with_any_tag('name', 'gmosx')
52
+
53
+ t = Article::Tag.find_by_name('ruby')
54
+ t.articles
55
+ t.articles.count
56
+
57
+ For an example usage of this Mixin, consult the Spark sources.
58
+
59
+ * Added support for relative and absolute URLs in redirects
60
+ and renders. This feature simplifies the creation of reusable
61
+ components.
62
+
63
+ * Support for assigning compound objects from the request. Here
64
+ is an example:
65
+
66
+ class Article
67
+ property :title, String
68
+ property :body, String
69
+ end
70
+
71
+ <form>
72
+ <input type="text" name="article.title" />
73
+ <input type="text" name="article.body" />
74
+ </form>
75
+
76
+ article = request.assign('article')
77
+
78
+ Alternatively you can use the article[title] article[body]
79
+ notation.
80
+
81
+ * Added simple Benchmarking mixin.
82
+
83
+ * Added support for 'evolving' a single Og managed class. Useful
84
+ when you are in development mode and change your schema.
85
+
86
+ * Added support for session garbage collection.
87
+
88
+ * Many many small bug fixes in Og and Nitro.
89
+
90
+
1
91
  == Version 0.22.0
2
92
 
3
93
  A snapshot of the latest developments. Many requested features
@@ -1,21 +1,12 @@
1
1
  # = Nitro
2
2
  #
3
- # Nitro is an efficient, yet simple engine for developing
4
- # professional Web Applications using Ruby and Javascript.
5
- # Nitro provides a robust infrastructure for scalable
6
- # applications that can be distributed over a server
7
- # cluster. However, Nitro can also power simple web
8
- # applications for deployment on intranets or desktops.
9
- #
10
- # Nitro integrates the powerful Og Object-Relational mapping
11
- # library.
12
- #
13
- # Copyright (c) 2004-2005, George Moschovitis (http://www.gmosx.com)
14
3
  # Copyright (c) 2004-2005, Navel Ltd (http://www.navel.gr)
4
+ # Copyright (c) 2004-2005, George Moschovitis (http://www.gmosx.com)
15
5
  #
16
- # Nitro is copyrighted free software created and maintained by
17
- # George Moschovitis (mailto:gm@navel.gr) and released under the
18
- # standard BSD Licence. For details consult the file doc/LICENCE.
6
+ # Nitro (http://www.nitrohq.com) is copyrighted free software
7
+ # created and maintained by George Moschovitis (mailto:gm@navel.gr)
8
+ # and released under the standard BSD Licence. For details
9
+ # consult the file doc/LICENCE.
19
10
 
20
11
  require 'glue'
21
12
  require 'glue/logger'
@@ -25,7 +16,7 @@ module Nitro
25
16
 
26
17
  # The version.
27
18
 
28
- Version = '0.22.0'
19
+ Version = '0.23.0'
29
20
 
30
21
  # Library path.
31
22
 
@@ -75,5 +66,3 @@ module Nitro
75
66
  end
76
67
 
77
68
  end
78
-
79
- # * George Moschovitis <gm@navel.gr>
@@ -103,6 +103,9 @@ class CgiUtils
103
103
  #
104
104
  # Parameters in the form xxx[] are converted
105
105
  # to arrays.
106
+ #
107
+ # Use the field.attr or field[attr] notation to pass
108
+ # compound objects.
106
109
 
107
110
  def self.parse_query_string(query_string)
108
111
  params = {}
@@ -117,12 +120,20 @@ class CgiUtils
117
120
  val = CGI.unescape(val) unless val.nil?
118
121
 
119
122
  if key =~ /(.*)\[\]$/
123
+ # Multiple values, for example a checkbox collection.
124
+ # Stored as an array.
120
125
  if params.has_key?($1)
121
126
  params[$1] << val
122
127
  else
123
128
  params[$1] = [val]
124
129
  end
130
+ elsif key =~ /(.*)\[(.*)\]$/ or key =~ /(.*)\.(.*)$/
131
+ # A compound object with attributes.
132
+ # Stored as a Hash.
133
+ params[$1] ||= {}
134
+ params[$1][$2] = val
125
135
  else
136
+ # Standard single valued parameter.
126
137
  params[key] = val.nil? ? nil : val
127
138
  end
128
139
  end
@@ -26,7 +26,11 @@ class Webrick
26
26
  else
27
27
  wblog = STDERR
28
28
  end
29
- @webrick = WEBrick::HTTPServer.new(
29
+
30
+ webrick_options = server.options.dup
31
+ require 'webrick/https' if webrick_options[:SSLEnable]
32
+
33
+ webrick_options.update(
30
34
  :BindAddress => server.address,
31
35
  :Port => server.port,
32
36
  :DocumentRoot => server.public_root,
@@ -35,6 +39,7 @@ class Webrick
35
39
  [wblog, WEBrick::AccessLog::REFERER_LOG_FORMAT]
36
40
  ]
37
41
  )
42
+ @webrick = WEBrick::HTTPServer.new(webrick_options)
38
43
 
39
44
  trap('INT') { @webrick.shutdown }
40
45
 
@@ -159,3 +164,4 @@ end
159
164
  end
160
165
 
161
166
  # * George Moschovitis <gm@navel.gr>
167
+ # * Guillaume Pierronnet <guillaume.pierronnet@laposte.net>
@@ -1,10 +1,6 @@
1
- # * George Moschovitis <gm@navel.gr>
2
- # (c) 2004-2005 Navel, all rights reserved.
3
- # $Id: stores.rb 182 2005-07-22 10:07:50Z gmosx $
4
-
5
1
  require 'fileutils'
6
2
 
7
- require 'glue/hash'
3
+ require 'mega/synchash'
8
4
 
9
5
  module Nitro
10
6
 
@@ -14,7 +10,7 @@ module Caching
14
10
 
15
11
  # Cached fragments are stored in memory.
16
12
 
17
- class MemoryStore < Glue::SafeHash
13
+ class MemoryStore < SyncHash
18
14
 
19
15
  def read(name, options = {})
20
16
  self[name]
@@ -82,3 +78,5 @@ module Caching
82
78
  end
83
79
 
84
80
  end
81
+
82
+ # * George Moschovitis <gm@navel.gr>
@@ -1,4 +1,4 @@
1
- require 'facet/object/singleton_class'
1
+ require 'nano/object/singleton_class'
2
2
 
3
3
  require 'glue/template'
4
4
  require 'nitro/compiler/errors'
@@ -1,4 +1,4 @@
1
- require 'facet/string/demodulize'
1
+ require 'mega/orm_support'
2
2
 
3
3
  module Nitro
4
4
 
@@ -1,3 +1,5 @@
1
+ require 'nano/object/assign_with'
2
+
1
3
  require 'nitro/request'
2
4
  require 'nitro/response'
3
5
  require 'nitro/render'
@@ -79,23 +81,28 @@ class Context
79
81
  EXCLUDED_PARAMETERS = %w{ oid name }
80
82
 
81
83
  def fill(obj, name = nil)
82
- # if an object is passed create an instance.
84
+ # If an class is passed create an instance.
83
85
  obj = obj.new if obj.is_a?(Class)
84
86
 
85
87
  @params.each do |param, val|
86
88
  begin
87
89
  # gmosx: DO NOT escape by default !!!
88
90
  if not EXCLUDED_PARAMETERS.include?(param)
89
- obj.send("__force_#{param}", val)
91
+ if val.is_a? Hash
92
+ obj.send("__force_hash_#{param}", val)
93
+ else
94
+ obj.send("__force_#{param}", val)
95
+ end
90
96
  end
91
- rescue NameError
97
+ rescue NameError => ex
92
98
  next
93
99
  end
94
100
  end
95
-
101
+
96
102
  return obj
97
103
  end
98
104
  alias_method :populate, :fill
105
+ alias_method :assign, :fill
99
106
  end
100
107
 
101
108
  end
@@ -217,6 +217,3 @@ class SimpleController < Controller
217
217
  end
218
218
 
219
219
  end
220
-
221
- # * George Moschovitis <gm@navel.gr>
222
- # * James Britt <james_b@neurogami.com>
@@ -8,13 +8,14 @@ class Cookie
8
8
  attr_accessor :domain, :path, :secure
9
9
  attr_accessor :comment, :max_age
10
10
 
11
- def initialize(name, value)
11
+ def initialize(name = nil, value = nil, expires = nil)
12
12
  @name = name
13
13
  @value = value
14
+ self.expires = expires
14
15
  @version = 0 # Netscape Cookie
15
16
  @path = '/' # gmosx: KEEP this!
16
- @domain = @secure = @comment = @max_age =
17
- @expires = @comment_url = @discard = @port = nil
17
+ @domain = @secure = @comment = @max_age = nil
18
+ @comment_url = @discard = @port = nil
18
19
  end
19
20
 
20
21
  def expires=(t)
@@ -1,4 +1,4 @@
1
- require 'facet/object/special_class'
1
+ require 'nano/object/singleton_class'
2
2
 
3
3
  require 'nitro/controller'
4
4
  require 'nitro/routing'
@@ -86,6 +86,12 @@ class Dispatcher
86
86
  end
87
87
 
88
88
  auto_mixin(c)
89
+
90
+ # Perform mount-time initialization of the controller.
91
+
92
+ if c.respond_to? :mounted
93
+ c.mounted
94
+ end
89
95
 
90
96
  # Try to setup a template_root if none is defined:
91
97
 
@@ -8,6 +8,9 @@ class Dispatcher
8
8
 
9
9
  # An alternative dispatching algorithm that handles
10
10
  # implicit nice urls. Subdirectories are not supported.
11
+ #
12
+ # Returns the dispatcher class, the action name and the
13
+ # base url. For the root path, the base url is nil.
11
14
 
12
15
  def dispatch(path, context)
13
16
  path = route(path, context)
@@ -18,7 +21,7 @@ class Dispatcher
18
21
  if klass = controller_class_for("/#{parts.first}")
19
22
  base = "/#{parts.shift}"
20
23
  else
21
- base = ROOT
24
+ base = nil
22
25
  klass = controller_class_for(ROOT)
23
26
  end
24
27
 
@@ -30,6 +33,8 @@ class Dispatcher
30
33
  context.headers['QUERY_STRING'] = "#{parts.join(';')};#{context.headers['QUERY_STRING']}"
31
34
  end
32
35
 
36
+ base = nil if base == ROOT
37
+
33
38
  return klass, "#{action}_action", base
34
39
  end
35
40
  end
@@ -1,8 +1,8 @@
1
1
  require 'rexml/document'
2
2
  require 'rexml/streamlistener'
3
3
 
4
- require 'facet/string/capitalized%3F'
5
- require 'facet/string/camelize'
4
+ require 'nano/string/capitalized%3F'
5
+ require 'nano/string/camelize'
6
6
 
7
7
  require 'glue/configuration'
8
8
 
@@ -0,0 +1,16 @@
1
+ require 'benchmark'
2
+
3
+ module Nitro
4
+
5
+ module BenchmarkMixin
6
+
7
+ def benchmark(message = 'Benchmarking')
8
+ real = Benchmark.realtime { yield }
9
+ Logger.info "#{message}: time = #{'%.5f' % real} ms."
10
+ end
11
+
12
+ end
13
+
14
+ end
15
+
16
+ # * George Moschovitis <gm@navel.gr>
@@ -1,88 +1,204 @@
1
- require 'glue/hash'
1
+ require 'nano/inflect'
2
2
 
3
3
  module Nitro
4
4
 
5
5
  # A collection of useful helpers for creating and manipulating
6
6
  # Forms.
7
+ #--
8
+ # FIXME: cleanup this file, move stuff to scaffold, allow
9
+ # overrides.
10
+ #++
7
11
 
8
12
  module FormMixin
9
13
 
10
- # Render a standard form for the given Entity (ie an
11
- # object with attribute metadata).
14
+ def self.included(base)
15
+ base.send :include, XhtmlMixin
16
+ end
17
+
18
+ private
19
+
20
+ # Render a standard form for the given Object. The object
21
+ # should include attribute metadata.
22
+ #--
23
+ # TODO: get info, for example localization mode from session,
24
+ # if this module is mixed in a Render.
25
+ #++
26
+
27
+ def form_for(obj, options = {})
28
+ method = options.fetch(:method, 'post')
29
+ action = options.fetch(:action, "save_#{obj.class.name.underscore}")
30
+ submit = options.fetch(:submit, 'Save')
31
+
32
+ action = "#{@base}/#{action}" unless action =~ /\//
33
+
34
+ str = %{<form action="#{action}" method="post">}
35
+ if obj.oid
36
+ str << %{
37
+ <input type="hidden" name="oid" value="#{obj.oid}" />}
38
+ end
39
+ str << %{
40
+ #{tags_for(obj, options)}
41
+ <br />
42
+ <input type="submit" value="#{submit}" /> or <a href="#@base/#{Scaffolding.class_to_list(obj.class)}">Cancel</a>
43
+ </form>
44
+ }
45
+ return str
46
+ end
47
+
48
+ # Render a standard form tags for the given Object. The object
49
+ # should include attribute metadata.
50
+ #
12
51
  # If show_all is false then apply field filtering.
13
52
  #
53
+ # For extra flexibility and to keep semantics this helper
54
+ # emits a <dl> structure. You can use CSS to style the
55
+ # list to fit your overal design.
56
+ #
14
57
  # Example:
15
58
  #
16
59
  # <p>
17
60
  # <form name="test">
18
- # #{form_for entry}
61
+ # #{tags_for entry}
19
62
  # </form>
20
63
  # </p>
21
- #--
22
- # TODO: get info, for example localization mode from session,
23
- # if this module is mixed in a Render.
24
- #++
25
64
 
26
- def form_for(obj, lc = nil, show_all = false)
27
- str = '<dl>'
65
+ def tags_for(obj, options = {})
66
+ str = prologue()
28
67
 
29
68
  for p in obj.class.properties
30
- unless show_all
31
- next if :oid == p.symbol
32
- end
33
-
34
- if p.klass.ancestors.include?(Integer) or
35
- p.klass.ancestors.include?(Float)
36
- str << %{
37
- <dt><label for="#{p.symbol}">#{p.symbol}</label></dt>
38
- <dd>
39
- <input type="text" id="#{p.symbol}" name="#{p.symbol}" value="#{obj.send(p.symbol)}" />
40
- </dd>
41
- }
42
- elsif p.klass.ancestors.include?(String)
43
- str << %{
44
- <dt><label for="#{p.symbol}">#{p.symbol}</label></dt>
45
- <dd>
46
- }
47
- val = obj.send(p.symbol)
48
- if :textarea == p.meta[:ui]
49
- str << %{
50
- <textarea id="#{p.symbol}" name="#{p.symbol}">#{val}</textarea>
51
- }
69
+ next if :oid == p.symbol unless options[:all]
70
+ ancestors = p.klass.ancestors
71
+ if ancestors.include?(Numeric)
72
+ unless p.meta[:relation]
73
+ str << field_tag(obj, p, options)
74
+ end
75
+ elsif ancestors.include?(String)
76
+ if p.metadata[:editor] == :textarea
77
+ str << textarea_tag(obj, p, options)
52
78
  else
53
- str << %{
54
- <input type="text" id="#{p.symbol}" name="#{p.symbol}" value="#{val}" />
55
- }
79
+ str << field_tag(obj, p, options)
56
80
  end
57
- str << %{
58
- </dd>
59
- }
60
- elsif p.klass.ancestors.include?(TrueClass)
61
- str << %{
62
- <dt><label for="#{p.symbol}">#{p.symbol}</label></dt>
81
+ elsif ancestors.include?(TrueClass)
82
+ str << checkbox_tag(obj, p, options)
83
+ elsif ancestors.include?(Time)
84
+ str << datetime_tag(obj, p, options)
85
+ end
86
+ end
87
+
88
+ for rel in obj.class.relations
89
+ case rel
90
+ when Og::BelongsTo
91
+ str << belongs_to_tag(obj, rel, options)
92
+ when Og::HasMany
93
+ str << has_many_tag(obj, rel, options)
94
+ end
95
+ end
96
+
97
+ str << epilogue()
98
+
99
+ return str
100
+ end
101
+
102
+ def belongs_to_tag(obj, rel, options)
103
+ entities = rel.target_class.all
104
+ labels = entities.map { |e| e.to_s }
105
+ values = entities.map { |e| e.oid }
106
+ element(
107
+ label(rel.name),
108
+ %{
109
+ <select id="#{rel.name}" name="#{rel.name}">
110
+ #{options(:labels => labels, :values => values, :selected => 1)}
111
+ </select>
112
+ }
113
+ )
114
+ end
115
+
116
+ def has_many_tag(obj, rel, options)
117
+ entities = obj.send(rel.target_plural_name) if obj.saved?
118
+ unless entities.empty?
119
+ str = entities.inject('') do |acc, e|
120
+ acc << "<tr><td>#{e.to_edit_link(@base)}</td></tr>"
121
+ end
122
+ str = "<table>#{str}</table>"
123
+ else
124
+ str = 'No entities found.<br /><br />'
125
+ end
126
+ element(
127
+ label(rel.name) + '&nbsp;<a href="#">Add</a>',
128
+ %{
129
+ #{str}
130
+ }
131
+ )
132
+ end
133
+
134
+ def field_tag(obj, p, options)
135
+ %{
136
+ <dt>#{label(p.symbol)}</dt>
63
137
  <dd>
64
- <input type="checkbox" id="#{p.symbol}" name="#{p.symbol}" />
138
+ <input type="text" id="#{p.symbol}" name="#{p.symbol}" value="#{obj.send(p.symbol)}" style="width: 250px" />
65
139
  </dd>
66
- }
67
- elsif p.klass.ancestors.include?(Time)
68
- str << %{
69
- <dt><label for="#{p.symbol}">#{p.symbol}</label></dt>
140
+ }
141
+ end
142
+
143
+ def textarea_tag(obj, p, options)
144
+ %{
145
+ <dt>#{label(p.symbol)}</dt>
70
146
  <dd>
71
- <input type="text" id="#{p.symbol}" name="#{p.symbol}" value="#{obj.send(p.symbol)}" />
147
+ <textarea id="#{p.symbol}" name="#{p.symbol}">#{obj.send(p.symbol)}</textarea>
72
148
  </dd>
73
- }
74
- end
75
- end
149
+ }
150
+ end
76
151
 
77
- str << %{
78
- </dl>}
152
+ def checkbox_tag(obj, p, options)
153
+ %{
154
+ <dt>
155
+ <input type="checkbox" id="#{p.symbol}" name="#{p.symbol}" />&nbsp;
156
+ #{label(p.symbol)}
157
+ </dt>
158
+ <dd>#{}</dd>
159
+ }
160
+ end
79
161
 
80
- self << str
162
+ def datetime_tag(obj, p, options)
163
+ element(
164
+ label(p.symbol),
165
+ datetime_select(obj.send(p.symbol), :name => p.symbol.to_s)
166
+ )
167
+ end
168
+
169
+ def label(sym)
170
+ %|<label for="#{sym}">#{sym.to_s.humanize}</label>|
171
+ end
172
+
173
+ # :section: Relations.
174
+
175
+
176
+ # :section: General formating methods. Override to customize.
177
+
178
+ # Emit form prologue.
179
+
180
+ def prologue
181
+ '<dl>'
182
+ end
183
+
184
+ # Emit form epilogue.
185
+
186
+ def epilogue
187
+ '</dl>'
81
188
  end
82
- alias_method :build_form, :form_for
83
189
 
190
+ # Emit a form element.
191
+
192
+ def element(label, element)
193
+ %{
194
+ <dt>#{label}</dt>
195
+ <dd>
196
+ #{element}
197
+ </dd>
198
+ }
199
+ end
84
200
  end
85
201
 
86
202
  end
87
203
 
88
- # * George Moschovitis
204
+ # * George Moschovitis <gm@navel.gr>