htmlfilter 1.0.0 → 1.1

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/HISTORY ADDED
@@ -0,0 +1,25 @@
1
+ = RELEASE HISTORY
2
+
3
+ == 1.1 / 2009-11-24
4
+
5
+ This is release adjusts the names of the classes to
6
+ be capitialized according to the actual use of the
7
+ terms. Some alternate options presets have been added
8
+ as well, and this releaseo sheds the Multiton, which
9
+ was basically a YAGNI.
10
+
11
+ Changes:
12
+
13
+ * Renamed HtmlFilter to HTMLFilter.
14
+ * Renamed CssFilter to CSSFilter
15
+ * HTMLFilter is no longer a Multiton.
16
+ * Old names are still available temporarily.
17
+ * Added built-in option constants.
18
+ * CssTree is now CSSFilter::Tree.
19
+
20
+ == 1.0.0 / 2009-06-25
21
+
22
+ Changes:
23
+
24
+ * Birthday! (Spun-off from Ruby Facets)
25
+
@@ -1,19 +1,17 @@
1
1
  #!mast bin lib meta test [A-Z]*
2
- lib
3
2
  lib/cssfilter.rb
4
- lib/htmlfilter
5
- lib/htmlfilter/multiton.rb
6
3
  lib/htmlfilter.rb
7
- meta
8
- meta/package
9
- meta/project
4
+ meta/collection
5
+ meta/contact
6
+ meta/description
7
+ meta/homepage
8
+ meta/name
9
+ meta/repository
10
10
  meta/title
11
11
  meta/version
12
- test
13
12
  test/test_cssfilter.rb
14
13
  test/test_htmlfilter.rb
15
14
  Rakefile
16
- Manifest.txt
17
15
  TODO
18
16
  README.rdoc
19
- History.rdoc
17
+ HISTORY
data/README.rdoc CHANGED
@@ -1,6 +1,7 @@
1
1
  = HtmlFilter
2
2
 
3
3
  * http://rubyworks.github.com/htmlfilter
4
+ * http://github.com/rubyworks/htmlfilter
4
5
 
5
6
  == DESCRIPTION:
6
7
 
@@ -14,8 +15,11 @@ whitespace and most importantly remove urls.
14
15
 
15
16
  == FEATURES:
16
17
 
17
- * Santize HTML
18
- * Compress CSS
18
+ * Based on well-worn PHP library.
19
+ * Regular expression based filtering.
20
+ * Very efficient for small snippets, like blog comments.
21
+ * Pure-Ruby and no dependencies.
22
+ * Also has library to clean and compact cascading stylesheets.
19
23
 
20
24
  == SYNOPSIS:
21
25
 
@@ -27,14 +31,10 @@ Via the class.
27
31
 
28
32
  Or using the String extension.
29
33
 
30
- html.html_filter #=> "<b>hello</b>"
34
+ html.html_filter(options) #=> "<b>hello</b>"
31
35
 
32
36
  See RDocs for more information.
33
37
 
34
- == REQUIREMENTS:
35
-
36
- * Uses a copy of multiton.rb (included)
37
-
38
38
  == INSTALL:
39
39
 
40
40
  * sudo gem install htmlfilter
data/TODO CHANGED
@@ -2,6 +2,4 @@
2
2
 
3
3
  * Maybe write executable(s) to use library via commandline.
4
4
  * Elaborate on Features list in README.txt.
5
- * Rename class to HTMLFilter (instead of HtmlFilter)
6
-
7
5
 
data/lib/cssfilter.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # = CSS Filter
2
2
  #
3
- # The CssFilter class will clean up a cascading style sheet.
3
+ # The CSSFilter class will clean up a cascading stylesheet.
4
4
  # It can be used to remove whitespace and most importantly
5
5
  # remove urls.
6
6
  #
@@ -8,25 +8,27 @@
8
8
  #
9
9
  # * Trans
10
10
  #
11
- # == Todo
11
+ # == Copying
12
12
  #
13
- # * Allow urls to be specified per attribute type.
13
+ # Copyright (c) 2007 Thomas Sawyer
14
14
  #
15
- # == Copying
15
+ # Creative Commons Attribution-ShareAlike 3.0 License
16
16
  #
17
- # Copyright (c) 2007 7rans
17
+ # Ref. http://creativecommons.org/licenses/by-sa/3.0/
18
+
19
+
20
+ # TODO: Allow urls to be specified per attribute type.
18
21
 
19
22
  #require 'htmlfilter/uri'
20
23
  require 'uri'
21
24
 
22
25
  # = CSS Filter
23
26
  #
24
- # The CssFilter class will clean up a cascading style sheet.
27
+ # The CSSFilter class will clean up a cascading style sheet.
25
28
  # It can be used to remove whitespace and most importantly
26
29
  # remove urls.
27
- #
28
- class CssFilter
29
- VERSION="1.0.0"
30
+
31
+ class CSSFilter
30
32
 
31
33
  # should we remove comments? (true, false)
32
34
  attr_accessor :strip_comments
@@ -158,7 +160,7 @@ class CssFilter
158
160
  # TODO: Not complete, does not work with "@xxx foo;" for example.
159
161
 
160
162
  def parse(css)
161
- tree = CssTree.new
163
+ tree = Tree.new
162
164
  entries = css.scan(/^(.*?)\{(.*?)\}/m)
163
165
  entries.each do |ref, props|
164
166
  tree[ref.strip] ||= {}
@@ -196,31 +198,33 @@ class CssFilter
196
198
  return val
197
199
  end
198
200
 
199
- end
200
-
201
-
202
- # CSS parse tree. This is for a "deep filtering".
201
+ # CSS parse tree. This is for a "deep filtering".
203
202
 
204
- class CssTree < Hash
203
+ class Tree < Hash
205
204
 
206
- def initialize(options=nil)
207
- @options = options || {}
208
- super()
209
- end
205
+ def initialize(options=nil)
206
+ @options = options || {}
207
+ super()
208
+ end
210
209
 
211
- # Re-output the CSS, all tidy ;)
210
+ # Re-output the CSS, all tidy ;)
212
211
 
213
- def to_css
214
- css = ""
215
- each do |selector, entries|
216
- css << "#{selector}{"
217
- entries.each do |key, value|
218
- css << "#{key}:#{value};"
212
+ def to_css
213
+ css = ""
214
+ each do |selector, entries|
215
+ css << "#{selector}{"
216
+ entries.each do |key, value|
217
+ css << "#{key}:#{value};"
218
+ end
219
+ css << "}\n"
219
220
  end
220
- css << "}\n"
221
+ return css
221
222
  end
222
- return css
223
+
223
224
  end
224
225
 
225
226
  end
226
227
 
228
+ # For backward compatability. Eventually this will be deprecated.
229
+ CssFilter = CSSFilter
230
+
data/lib/htmlfilter.rb CHANGED
@@ -5,9 +5,8 @@
5
5
  # for instance.
6
6
  #
7
7
  # HtmlFilter is a port of lib_filter.php, v1.15 by Cal Henderson <cal@iamcal.com>
8
- #
9
- # This code is licensed under a Creative Commons Attribution-ShareAlike 2.5 License
10
- # http://creativecommons.org/licenses/by-sa/2.5/
8
+ # licensed under a Creative Commons Attribution-ShareAlike 2.5 License
9
+ # http://creativecommons.org/licenses/by-sa/2.5/.
11
10
  #
12
11
  # Thanks to Jang Kim for adding support for single quoted attributes.
13
12
  #
@@ -26,32 +25,35 @@
26
25
  #
27
26
  # == Copying
28
27
  #
29
- # Copyright (c) 2007 Trans
28
+ # Copyright (c) 2007 Thomas Sawyer
29
+ #
30
+ # Creative Commons Attribution-ShareAlike 3.0 License
31
+ #
32
+ # Ref. http://creativecommons.org/licenses/by-sa/3.0/
30
33
 
31
- require 'htmlfilter/multiton.rb'
32
34
 
33
- # = HtmlFilter
35
+ # = HTMLFilter
34
36
  #
35
37
  # HTML Filter library can be used to sanitize and sterilize
36
38
  # HTML. A good idea if you let users submit HTML in comments,
37
39
  # for instance.
38
40
  #
39
- # lib_filter.php, v1.15 by Cal Henderson <cal@iamcal.com>
41
+ # == Usage
40
42
  #
41
- # This code is licensed under a Creative Commons Attribution-ShareAlike 2.5 License
42
- # http://creativecommons.org/licenses/by-sa/2.5/
43
- #
44
- # Thanks to Jang Kim for adding support for single quoted attributes.
43
+ # hf = HTMLFilter.new
44
+ # hf.filter("<b>Bold Action") #=> "<b>Bold Action</b>"
45
45
  #
46
46
  # == Reference
47
47
  #
48
48
  # * http://iamcal.com/publish/articles/php/processing_html/
49
49
  # * http://iamcal.com/publish/articles/php/processing_html_part_2/
50
-
51
- class HtmlFilter
52
- VERSION = "1.0.0"
53
-
54
- include Multiton
50
+ #
51
+ # == Issues
52
+ #
53
+ # * The built in option constants could use a fair bit of refinement.
54
+ # * Eventually the old HtmlFilter name needs to be deprecated.
55
+ #
56
+ class HTMLFilter
55
57
 
56
58
  # tags and attributes that are allowed
57
59
  #
@@ -85,7 +87,7 @@ class HtmlFilter
85
87
  # should we remove comments? (true, false)
86
88
  attr_accessor :strip_comments
87
89
 
88
- # should we try and make a b tag out of "b>" (true, false)
90
+ # should we try and make a <b> tag out of "b>" (true, false)
89
91
  attr_accessor :always_make_tags
90
92
 
91
93
  # entity control option (true, false)
@@ -94,14 +96,18 @@ class HtmlFilter
94
96
  # entity control option (amp, gt, lt, quot, etc.)
95
97
  attr_accessor :allowed_entities
96
98
 
97
- # default settings
99
+ ## max number of text characters at which to truncate (leave as +nil+ for no truncation)
100
+ #attr_accessor :truncate
98
101
 
102
+ # Default settings
99
103
  DEFAULT = {
100
104
  'allowed' => {
101
105
  'a' => ['href', 'target'],
106
+ 'img' => ['src', 'width', 'height', 'alt'],
102
107
  'b' => [],
103
108
  'i' => [],
104
- 'img' => ['src', 'width', 'height', 'alt']
109
+ 'em' => [],
110
+ 'tt' => [],
105
111
  },
106
112
  'no_close' => ['img', 'br', 'hr'],
107
113
  'always_close' => ['a', 'b'],
@@ -114,9 +120,75 @@ class HtmlFilter
114
120
  'allowed_entities' => ['amp', 'gt', 'lt', 'quot']
115
121
  }
116
122
 
117
- # New html filter.
123
+ # Basic settings are simlialr to DEFAULT but do not allow any type
124
+ # of links, neither <tt>a href</tt> or <tt>img</tt>.
125
+ BASIC = {
126
+ 'allowed' => {
127
+ 'b' => [],
128
+ 'i' => [],
129
+ 'em' => [],
130
+ 'tt' => [],
131
+ },
132
+ 'no_close' => ['img', 'br', 'hr'],
133
+ 'always_close' => ['a', 'b'],
134
+ 'protocol_attributes' => ['src', 'href'],
135
+ 'allowed_protocols' => ['http', 'ftp', 'mailto'],
136
+ 'remove_blanks' => ['a', 'b'],
137
+ 'strip_comments' => true,
138
+ 'always_make_tags' => true,
139
+ 'allow_numbered_entities' => true,
140
+ 'allowed_entities' => ['amp', 'gt', 'lt', 'quot']
141
+ }
142
+
143
+ # Strict settings do not allow any tags.
144
+ STRICT = {
145
+ 'allowed' => {},
146
+ 'no_close' => ['img', 'br', 'hr'],
147
+ 'always_close' => ['a', 'b'],
148
+ 'protocol_attributes' => ['src', 'href'],
149
+ 'allowed_protocols' => ['http', 'ftp', 'mailto'],
150
+ 'remove_blanks' => ['a', 'b'],
151
+ 'strip_comments' => true,
152
+ 'always_make_tags' => true,
153
+ 'allow_numbered_entities' => true,
154
+ 'allowed_entities' => ['amp', 'gt', 'lt', 'quot']
155
+ }
118
156
 
119
- def initialize( options=nil )
157
+ # Relaxed settings allows a great deal of HTML spec.
158
+ #
159
+ # TODO: Need to expand upon RELAXED options.
160
+ #
161
+ RELAXED = {
162
+ 'allowed' => {
163
+ 'a' => ['class', 'href', 'target'],
164
+ 'b' => ['class'],
165
+ 'i' => ['class'],
166
+ 'img' => ['class', 'src', 'width', 'height', 'alt'],
167
+ 'div' => ['class'],
168
+ 'pre' => ['class'],
169
+ 'code' => ['class'],
170
+ 'ul' => ['class'], 'ol' => ['class'], 'li' => ['class']
171
+ },
172
+ 'no_close' => ['img', 'br', 'hr'],
173
+ 'always_close' => ['a', 'b'],
174
+ 'protocol_attributes' => ['src', 'href'],
175
+ 'allowed_protocols' => ['http', 'ftp', 'mailto'],
176
+ 'remove_blanks' => ['a', 'b'],
177
+ 'strip_comments' => true,
178
+ 'always_make_tags' => true,
179
+ 'allow_numbered_entities' => true,
180
+ 'allowed_entities' => ['amp', 'gt', 'lt', 'quot']
181
+ }
182
+
183
+ # New html filter.
184
+ #
185
+ # Provide custom +options+, or use one of the built-in options
186
+ # constants.
187
+ #
188
+ # hf = HTMLFilter.new(HTMLFilter::RELAXED)
189
+ # hf.filter(htmlstr)
190
+ #
191
+ def initialize(options=nil)
120
192
  if options
121
193
  h = DEFAULT.dup
122
194
  options.each do |k,v|
@@ -126,22 +198,20 @@ class HtmlFilter
126
198
  else
127
199
  options = DEFAULT.dup
128
200
  end
129
-
130
201
  options.each{ |k,v| send("#{k}=",v) }
131
202
  end
132
203
 
133
204
  # Filter html string.
134
205
 
135
- def filter(data)
206
+ def filter(html)
136
207
  @tag_counts = {}
137
-
138
- data = escape_comments(data)
139
- data = balance_html(data)
140
- data = check_tags(data)
141
- data = process_remove_blanks(data)
142
- data = validate_entities(data)
143
-
144
- return data
208
+ html = escape_comments(html)
209
+ html = balance_html(html)
210
+ html = check_tags(html)
211
+ html = process_remove_blanks(html)
212
+ html = validate_entities(html)
213
+ #html = truncate_html(html)
214
+ html
145
215
  end
146
216
 
147
217
  private
@@ -504,13 +574,47 @@ class HtmlFilter
504
574
  return data
505
575
  end
506
576
 
577
+ ## HTML comment regular expression
578
+ #REM_RE = %r{<\!--(.*?)-->}
579
+ #
580
+ ## HTML tag regular expression
581
+ #TAG_RE = %r{</?\w+((\s+\w+(\s*=\s*(?:"(.|\n)*?"|'(.|\n)*?'|[^'">\s]+))?)+\s*|\s*)/?>} #'
582
+ #
583
+ ##
584
+ #def truncate_html(html)
585
+ # return html unless truncate
586
+ # # default settings
587
+ # limit = truncate
588
+ #
589
+ # mask = html.gsub(REM_RE){ |m| "\0" * m.size }
590
+ # mask = mask.gsub(TAG_RE){ |m| "\0" * m.size }
591
+ #
592
+ # i, x = 0, 0
593
+ #
594
+ # while i < mask.size && x < limit
595
+ # x += 1 if mask[i] != "\0"
596
+ # i += 1
597
+ # end
598
+ #
599
+ # while x > 0 && mask[x,1] == "\0"
600
+ # x -= 1
601
+ # end
602
+ #
603
+ # return html[0..x]
604
+ #end
605
+
507
606
  end
508
607
 
509
608
  # Overload the standard String class for extra convienience.
510
609
 
511
610
  class String
512
611
  def html_filter(*opts)
513
- HtmlFilter.new(*opts).filter(self)
612
+ HTMLFilter.new(*opts).filter(self)
514
613
  end
515
614
  end
516
615
 
616
+ # For backward compatability. Eventually this will be deprecated.
617
+ HtmlFilter = HTMLFilter
618
+
619
+
620
+
File without changes
data/meta/contact ADDED
@@ -0,0 +1 @@
1
+ rubyworks-mailinglist@googlegroups.com
data/meta/description ADDED
@@ -0,0 +1 @@
1
+ Sanitize and sterilize HTML, also includes a CSS filter.
data/meta/homepage ADDED
@@ -0,0 +1 @@
1
+ http://rubyworks.github.com/htmlfilter
File without changes
data/meta/repository ADDED
@@ -0,0 +1 @@
1
+ git://github.com/rubyworks/htmlfilter.git
data/meta/version CHANGED
@@ -1 +1 @@
1
- 1.0.0
1
+ 1.1
@@ -2,7 +2,7 @@ require "test/unit"
2
2
  require "cssfilter"
3
3
  #require 'yaml'
4
4
 
5
- class TestCssFilter < Test::Unit::TestCase
5
+ class TestCSSFilter < Test::Unit::TestCase
6
6
 
7
7
  def setup
8
8
  @css = <<-END
@@ -26,7 +26,7 @@ class TestCssFilter < Test::Unit::TestCase
26
26
  end
27
27
 
28
28
  def test_filter
29
- cssfilter = CssFilter.new(:allowed_hosts=>["here.org"], :strip_whitespace => true)
29
+ cssfilter = CSSFilter.new(:allowed_hosts=>["here.org"], :strip_whitespace => true)
30
30
  csstree = cssfilter.filter(@css)
31
31
  assert_equal(@result, csstree.to_s)
32
32
  end
@@ -1,28 +1,12 @@
1
1
  require "test/unit"
2
2
  require "htmlfilter"
3
3
 
4
- class TestHtmlFilter < Test::Unit::TestCase
4
+ class TestHTMLFilter < Test::Unit::TestCase
5
5
 
6
6
  # core tests
7
7
 
8
- def test_multiton_without_options
9
- h1 = HtmlFilter.new
10
- h2 = HtmlFilter.new
11
- h3 = HtmlFilter.new( :strip_comments => false )
12
- assert_equal( h1.object_id, h2.object_id )
13
- assert_not_equal( h1.object_id, h3.object_id )
14
- end
15
-
16
- def test_multiton_with_options
17
- h1 = HtmlFilter.new( :strip_comments => false )
18
- h2 = HtmlFilter.new( :strip_comments => false )
19
- h3 = HtmlFilter.new
20
- assert_equal( h1.object_id, h2.object_id )
21
- assert_not_equal( h1.object_id, h3.object_id )
22
- end
23
-
24
8
  def test_strip_single
25
- hf = HtmlFilter.new
9
+ hf = HTMLFilter.new
26
10
  assert_equal( '"', hf.send(:strip_single,'\"') )
27
11
  assert_equal( "\000", hf.send(:strip_single,'\0') )
28
12
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: htmlfilter
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: "1.1"
5
5
  platform: ruby
6
6
  authors: []
7
7
 
@@ -9,46 +9,48 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-09-22 00:00:00 -04:00
12
+ date: 2009-11-24 00:00:00 -05:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
16
- description: HTML Filter library can be used to sanitize and sterilize HTML. A good idea if you let users submit HTML in comments, for instance. This library also include CssFilter. The CssFilter class will clean-up a cascading style sheet. It can be used to remove whitespace and most importantly remove urls.
17
- email:
16
+ description: Sanitize and sterilize HTML, also includes a CSS filter.
17
+ email: rubyworks-mailinglist@googlegroups.com
18
18
  executables: []
19
19
 
20
20
  extensions: []
21
21
 
22
22
  extra_rdoc_files:
23
23
  - Rakefile
24
- - Manifest.txt
24
+ - MANIFEST
25
25
  - TODO
26
26
  - README.rdoc
27
- - History.rdoc
27
+ - HISTORY
28
28
  files:
29
29
  - lib/cssfilter.rb
30
- - lib/htmlfilter/multiton.rb
31
30
  - lib/htmlfilter.rb
32
- - meta/package
33
- - meta/project
31
+ - meta/collection
32
+ - meta/contact
33
+ - meta/description
34
+ - meta/homepage
35
+ - meta/name
36
+ - meta/repository
34
37
  - meta/title
35
38
  - meta/version
36
39
  - test/test_cssfilter.rb
37
40
  - test/test_htmlfilter.rb
38
41
  - Rakefile
39
- - Manifest.txt
40
42
  - TODO
41
43
  - README.rdoc
42
- - History.rdoc
44
+ - HISTORY
45
+ - MANIFEST
43
46
  has_rdoc: true
44
- homepage:
47
+ homepage: http://rubyworks.github.com/htmlfilter
45
48
  licenses: []
46
49
 
47
50
  post_install_message:
48
51
  rdoc_options:
49
- - --inline-source
50
52
  - --title
51
- - htmlfilter api
53
+ - HTMLFilter API
52
54
  require_paths:
53
55
  - lib
54
56
  required_ruby_version: !ruby/object:Gem::Requirement
@@ -69,7 +71,7 @@ rubyforge_project: htmlfilter
69
71
  rubygems_version: 1.3.5
70
72
  signing_key:
71
73
  specification_version: 3
72
- summary: HTML Filter library can be used to sanitize and sterilize HTML.
74
+ summary: Sanitize and sterilize HTML, also includes a CSS filter.
73
75
  test_files:
74
76
  - test/test_cssfilter.rb
75
77
  - test/test_htmlfilter.rb
data/History.rdoc DELETED
@@ -1,6 +0,0 @@
1
- === 1.0.0 / 2009-06-25
2
-
3
- * 1 major enhancement
4
-
5
- * Birthday!
6
-
@@ -1,386 +0,0 @@
1
- # = Multiton
2
- #
3
- # == Synopsis
4
- #
5
- # Multiton design pattern ensures only one object is allocated for a given state.
6
- #
7
- # The 'multiton' pattern is similar to a singleton, but instead of only one
8
- # instance, there are several similar instances. It is useful when you want to
9
- # avoid constructing objects many times because of some huge expense (connecting
10
- # to a database for example), require a set of similar but not identical
11
- # objects, and cannot easily control how many times a contructor may be called.
12
- #
13
- # class SomeMultitonClass
14
- # include Multiton
15
- # attr :arg
16
- # def initialize(arg)
17
- # @arg = arg
18
- # end
19
- # end
20
- #
21
- # a = SomeMultitonClass.new(4)
22
- # b = SomeMultitonClass.new(4) # a and b are same object
23
- # c = SomeMultitonClass.new(2) # c is a different object
24
- #
25
- # == Previous Behavior
26
- #
27
- # In previous versions of Multiton the #new method was made
28
- # private and #instance had to be used in its stay --just like Singleton.
29
- # But this is less desirable for Multiton since Multitions can
30
- # have multiple instances, not just one.
31
- #
32
- # So instead Multiton now defines #create as a private alias of
33
- # the original #new method (just in case it is needed) and then
34
- # defines #new to handle the multiton; #instance is provided
35
- # as an alias for it.
36
- #
37
- #--
38
- # So if you must have the old behavior, all you need do is re-alias
39
- # #new to #create and privatize it.
40
- #
41
- # class SomeMultitonClass
42
- # include Multiton
43
- # alias_method :new, :create
44
- # private :new
45
- # ...
46
- # end
47
- #
48
- # Then only #instance will be available for creating the Multiton.
49
- #++
50
- #
51
- # == How It Works
52
- #
53
- # A pool of objects is searched for a previously cached object,
54
- # if one is not found we construct one and cache it in the pool
55
- # based on class and the args given to the contructor.
56
- #
57
- # A limitation of this approach is that it is impossible to
58
- # detect if different blocks were given to a contructor (if it takes a
59
- # block). So it is the constructor arguments _only_ which determine
60
- # the uniqueness of an object. To workaround this, define the _class_
61
- # method ::multiton_id.
62
- #
63
- # def Klass.multiton_id(*args, &block)
64
- # # ...
65
- # end
66
- #
67
- # Which should return a hash key used to identify the object being
68
- # constructed as (not) unique.
69
- #
70
- # == Authors
71
- #
72
- # * Christoph Rippel
73
- # * Thomas Sawyer
74
- #
75
- # = Copying
76
- #
77
- # Copyright (c) 2007 Christoph Rippel, Thomas Sawyer
78
- #
79
- # Ruby License
80
- #
81
- # This module is free software. You may use, modify, and/or redistribute this
82
- # software under the same terms as Ruby.
83
- #
84
- # This program is distributed in the hope that it will be useful, but WITHOUT
85
- # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
86
- # FOR A PARTICULAR PURPOSE.
87
-
88
- require 'thread'
89
-
90
- # = Multiton
91
- #
92
- # Multiton design pattern ensures only one object is allocated for a given state.
93
- #
94
- # The 'multiton' pattern is similar to a singleton, but instead of only one
95
- # instance, there are several similar instances. It is useful when you want to
96
- # avoid constructing objects many times because of some huge expense (connecting
97
- # to a database for example), require a set of similar but not identical
98
- # objects, and cannot easily control how many times a contructor may be called.
99
- #
100
- # class SomeMultitonClass
101
- # include Multiton
102
- # attr :arg
103
- # def initialize(arg)
104
- # @arg = arg
105
- # end
106
- # end
107
- #
108
- # a = SomeMultitonClass.new(4)
109
- # b = SomeMultitonClass.new(4) # a and b are same object
110
- # c = SomeMultitonClass.new(2) # c is a different object
111
- #
112
- # == How It Works
113
- #
114
- # A pool of objects is searched for a previously cached object,
115
- # if one is not found we construct one and cache it in the pool
116
- # based on class and the args given to the contructor.
117
- #
118
- # A limitation of this approach is that it is impossible to
119
- # detect if different blocks were given to a contructor (if it takes a
120
- # block). So it is the constructor arguments _only_ which determine
121
- # the uniqueness of an object. To workaround this, define the _class_
122
- # method ::multiton_id.
123
- #
124
- # def Klass.multiton_id(*args, &block)
125
- # # ...
126
- # end
127
- #
128
- # Which should return a hash key used to identify the object being
129
- # constructed as (not) unique.
130
-
131
- module Multiton
132
-
133
- # disable build-in copying methods
134
-
135
- def clone
136
- raise TypeError, "can't clone Multiton #{self}"
137
- #self
138
- end
139
-
140
- def dup
141
- raise TypeError, "can't dup Multiton #{self}"
142
- #self
143
- end
144
-
145
- # default marshalling strategy
146
-
147
- protected
148
-
149
- def _dump(depth=-1)
150
- Marshal.dump(@multiton_initializer)
151
- end
152
-
153
- # Mutex to safely store multiton instances.
154
-
155
- class InstanceMutex < Hash #:nodoc:
156
- def initialize
157
- @global = Mutex.new
158
- end
159
-
160
- def initialized(arg)
161
- store(arg, DummyMutex)
162
- end
163
-
164
- def (DummyMutex = Object.new).synchronize
165
- yield
166
- end
167
-
168
- def default(arg)
169
- @global.synchronize{ fetch(arg){ store(arg, Mutex.new) } }
170
- end
171
- end
172
-
173
- # Multiton can be included in another module, in which case that module effectively becomes
174
- # a multiton behavior distributor too. This is why we propogate #included to the base module.
175
- # by putting it in another module.
176
- #
177
- #--
178
- # def append_features(mod)
179
- # # help out people counting on transitive mixins
180
- # unless mod.instance_of?(Class)
181
- # raise TypeError, "Inclusion of Multiton in module #{mod}"
182
- # end
183
- # super
184
- # end
185
- #++
186
-
187
- module Inclusive
188
- private
189
- def included(base)
190
- class << base
191
- #alias_method(:new!, :new) unless method_defined?(:new!)
192
- # gracefully handle multiple inclusions of Multiton
193
- unless include?(Multiton::MetaMethods)
194
- alias_method :new!, :new
195
- private :allocate #, :new
196
- include Multiton::MetaMethods
197
-
198
- if method_defined?(:marshal_dump)
199
- undef_method :marshal_dump
200
- warn "warning: marshal_dump was undefined since it is incompatible with the Multiton pattern"
201
- end
202
- end
203
- end
204
- end
205
- end
206
-
207
- extend Inclusive
208
-
209
- #
210
-
211
- module MetaMethods
212
-
213
- include Inclusive
214
-
215
- def instance(*e, &b)
216
- arg = multiton_id(*e, &b)
217
- multiton_instance.fetch(arg) do
218
- multiton_mutex[arg].synchronize do
219
- multiton_instance.fetch(arg) do
220
- val = multiton_instance[arg] = new!(*e, &b) #new(*e, &b)
221
- val.instance_variable_set(:@multiton_initializer, e, &b)
222
- multiton_mutex.initialized(arg)
223
- val
224
- end
225
- end
226
- end
227
- end
228
- alias_method :new, :instance
229
-
230
- def initialized?(*e, &b)
231
- multiton_instance.key?(multiton_id(*e, &b))
232
- end
233
-
234
- protected
235
-
236
- def multiton_instance
237
- @multiton_instance ||= Hash.new
238
- end
239
-
240
- def multiton_mutex
241
- @multiton_mutex ||= InstanceMutex.new
242
- end
243
-
244
- def reinitialize
245
- multiton_instance.clear
246
- multiton_mutex.clear
247
- end
248
-
249
- def _load(str)
250
- instance(*Marshal.load(str))
251
- end
252
-
253
- private
254
-
255
- # Default method to to create a key to cache already constructed
256
- # instances. In the use case MultitonClass.new(e), MultiClass.new(f)
257
- # must be semantically equal if multiton_id(e).eql?(multiton_id(f))
258
- # evaluates to true.
259
- def multiton_id(*e, &b)
260
- e
261
- end
262
-
263
- def singleton_method_added(sym)
264
- super
265
- if (sym == :marshal_dump) & singleton_methods.include?('marshal_dump')
266
- raise TypeError, "Don't use marshal_dump - rely on _dump and _load instead"
267
- end
268
- end
269
-
270
- end
271
-
272
- end
273
-
274
-
275
-
276
-
277
- =begin
278
- # TODO Convert this into a real test and/or benchmark.
279
-
280
- if $0 == __FILE__
281
-
282
- ### Simple marshalling test #######
283
- class A
284
- def initialize(a,*e)
285
- @e = a
286
- end
287
-
288
- include Multiton
289
- begin
290
- def self.marshal_dump(depth = -1)
291
- end
292
- rescue => mes
293
- p mes
294
- class << self; undef marshal_dump end
295
- end
296
- end
297
-
298
- C = Class.new(A.clone)
299
- s = C.instance('a','b')
300
-
301
- raise unless Marshal.load(Marshal.dump(s)) == s
302
-
303
-
304
- ### Interdependent initialization example and threading benchmark ###
305
-
306
- class Regular_SymPlane
307
- def self.multiton_id(*e)
308
- a,b = e
309
- (a+b - 1)*(a+b )/2 + (a > b ? a : b)
310
- end
311
-
312
- def initialize(a,b)
313
- klass = self.class
314
- if a < b
315
- @l = b > 0 ? klass.instance(a,b-1) : nil
316
- @r = a > 0 ? klass.instance(a-1,b) : nil
317
- else
318
- @l = a > 0 ? klass.instance(a-1,b) : nil
319
- @r = b > 0 ? klass.instance(a,b-1) : nil
320
- end
321
- end
322
-
323
- include Multiton
324
- end
325
-
326
-
327
-
328
- def nap
329
- # Thread.pass
330
- sleep(rand(0.01))
331
- end
332
-
333
- class SymPlane < Regular_SymPlane
334
- @m = Mutex.new
335
- @count = 0
336
- end
337
-
338
- class << SymPlane
339
- attr_reader :count
340
- def reinitialize
341
- super
342
- @m = Mutex.new
343
- @count = 0
344
- end
345
- def inherited(sub_class)
346
- super
347
- sub_class.instance_eval { @m = Mutex.new; @count = 0 }
348
- end
349
-
350
- def multiton_id(*e)
351
- nap()
352
- super
353
- end
354
-
355
- def new!(*e) # NOTICE!!!
356
- super
357
- ensure
358
- nap()
359
- @m.synchronize { p @count if (@count += 1) % 15 == 0 }
360
- end
361
-
362
- def run(k)
363
- threads = 0
364
- max = k * (k+1) / 2
365
- puts ""
366
- while count() < max
367
- Thread.new { threads+= 1; instance(rand(30),rand(30)) }
368
- end
369
- puts "\nThe simulation created #{threads} threads"
370
- end
371
- end
372
-
373
-
374
- require 'benchmark'
375
- include Benchmark
376
-
377
- bmbm do |x|
378
- x.report('Initialize 465 SymPlane instances') { SymPlane.run(30) }
379
- x.report('Reinitialize ') do
380
- sleep 3
381
- SymPlane.reinitialize
382
- end
383
- end
384
-
385
- end
386
- =end