rUtilAnts 0.1.0.20091014

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,228 @@
1
+ #--
2
+ # Copyright (c) 2009 Muriel Salvan (murielsalvan@users.sourceforge.net)
3
+ # Licensed under the terms specified in LICENSE file. No warranty is provided.
4
+ #++
5
+
6
+ module RUtilAnts
7
+
8
+ module URLAccess
9
+
10
+ # Constants identifying which form is the content returned by URL handlers
11
+ CONTENT_ERROR = 0
12
+ CONTENT_REDIRECT = 1
13
+ CONTENT_STRING = 2
14
+ CONTENT_LOCALFILENAME = 3
15
+ CONTENT_LOCALFILENAME_TEMPORARY = 4
16
+
17
+ # Exception class handling redirection errors
18
+ class RedirectionError < RuntimeError
19
+ end
20
+
21
+ # Class
22
+ class Manager
23
+
24
+ # Constructor
25
+ def initialize
26
+ # Get the map of plugins to read URLs
27
+ # map< String, [ list<Regexp>, String ] >
28
+ # map< PluginName, [ List of matching regexps, Plugin class name ] >
29
+ @Plugins = {}
30
+ Dir.glob("#{File.dirname(__FILE__)}/URLHandlers/*.rb").each do |iFileName|
31
+ begin
32
+ lPluginName = File.basename(iFileName)[0..-4]
33
+ require "RUtilAnts/URLHandlers/#{lPluginName}"
34
+ @Plugins[lPluginName] = [
35
+ eval("RUtilAnts::URLCache::URLHandlers::#{lPluginName}::getMatchingRegexps"),
36
+ "RUtilAnts::URLCache::URLHandlers::#{lPluginName}"
37
+ ]
38
+ rescue Exception
39
+ logExc$!, "Error while requiring URLHandler plugin #{iFileName}"
40
+ end
41
+ end
42
+ end
43
+
44
+ # Access the content of a URL.
45
+ # No cache.
46
+ # It calls a code block with the binary content of the URL (or a local file name if required).
47
+ #
48
+ # Parameters:
49
+ # * *iURL* (_String_): The URL (used to detect cyclic redirections)
50
+ # * *iParameters* (<em>map<Symbol,Object></em>): Additional parameters:
51
+ # ** *:FollowRedirections* (_Boolean_): Do we follow redirections ? [optional = true]
52
+ # ** *:NbrRedirectionsAllowed* (_Integer_): Number of redirections allowed [optional = 10]
53
+ # ** *:LocalFileAccess* (_Boolean_): Do we need a local file to read the content from ? If not, the content itslef will be given the code block. [optional = false]
54
+ # ** *:URLHandler* (_Object_): The URL handler, if it has already been instantiated, or nil otherwise [optional = nil]
55
+ # * _CodeBlock_: The code returning the object corresponding to the content:
56
+ # ** *iContent* (_String_): File content, or file name if :LocalFileAccess was true
57
+ # ** *iFileBaseName* (_String_): The base name the file could have. Useful to get file name extensions.
58
+ # ** Returns:
59
+ # ** _Exception_: The error encountered, or nil in case of success
60
+ def accessFile(iURL, iParameters = {})
61
+ rError = nil
62
+
63
+ lFollowRedirections = iParameters[:lFollowRedirections]
64
+ lNbrRedirectionsAllowed = iParameters[:NbrRedirectionsAllowed]
65
+ lLocalFileAccess = iParameters[:LocalFileAccess]
66
+ lURLHandler = iParameters[:URLHandler]
67
+ if (lFollowRedirections == nil)
68
+ lFollowRedirections = true
69
+ end
70
+ if (lNbrRedirectionsAllowed == nil)
71
+ lNbrRedirectionsAllowed = 10
72
+ end
73
+ if (lLocalFileAccess == nil)
74
+ lLocalFileAccess = false
75
+ end
76
+ if (lURLHandler == nil)
77
+ lURLHandler = getURLHandler(iURL)
78
+ end
79
+ # Get the content from the handler
80
+ lContentFormat, lContent = lURLHandler.getContent(lFollowRedirections)
81
+ case (lContentFormat)
82
+ when CONTENT_ERROR
83
+ rError = lContent
84
+ when CONTENT_REDIRECT
85
+ # Handle too much redirections (cycles)
86
+ if (lContent.upcase == iURL.upcase)
87
+ rError = RedirectionError.new("Redirecting to the same URL: #{iURL}")
88
+ elsif (lNbrRedirectionsAllowed < 0)
89
+ rError = RedirectionError.new("Too much URL redirections for URL: #{iURL} redirecting to #{lContent}")
90
+ elsif (lFollowRedirections)
91
+ # Follow the redirection if we want it
92
+ lNewParameters = iParameters.clone
93
+ lNewParameters[:NbrRedirectionsAllowed] = lNbrRedirectionsAllowed - 1
94
+ # Reset the URL handler for the new parameters.
95
+ lNewParameters[:URLHandler] = nil
96
+ rError = accessFile(lContent, lNewParameters) do |iContent, iBaseName|
97
+ yield(iContent, iBaseName)
98
+ end
99
+ else
100
+ rError = RedirectionError.new("Received invalid redirection for URL: #{iURL}")
101
+ end
102
+ when CONTENT_STRING
103
+ # The content is directly accessible.
104
+ if (lLocalFileAccess)
105
+ # Write the content in a local temporary file
106
+ require 'tmpdir'
107
+ lBaseName = lURLHandler.getCorrespondingFileBaseName
108
+ lLocalFileName = "#{Dir.tmpdir}/URLCache/#{lBaseName}"
109
+ begin
110
+ require 'fileutils'
111
+ FileUtils::mkdir_p(File.dirname(lLocalFileName))
112
+ File.open(lLocalFileName, 'wb') do |oFile|
113
+ oFile.write(lContent)
114
+ end
115
+ rescue Exception
116
+ rError = $!
117
+ lContent = nil
118
+ end
119
+ if (rError == nil)
120
+ yield(lLocalFileName, lBaseName)
121
+ # Delete the temporary file
122
+ File.unlink(lLocalFileName)
123
+ end
124
+ else
125
+ # Give it to the code block directly
126
+ yield(lContent, lURLHandler.getCorrespondingFileBaseName)
127
+ end
128
+ when CONTENT_LOCALFILENAME, CONTENT_LOCALFILENAME_TEMPORARY
129
+ lLocalFileName = lContent
130
+ # The content is a local file name already accessible
131
+ if (!lLocalFileAccess)
132
+ # First, read the local file name
133
+ begin
134
+ File.open(lLocalFileName, 'rb') do |iFile|
135
+ # Replace the file name with the real content
136
+ lContent = iFile.read
137
+ end
138
+ rescue Exception
139
+ rError = $!
140
+ end
141
+ end
142
+ if (rError == nil)
143
+ yield(lContent, lURLHandler.getCorrespondingFileBaseName)
144
+ end
145
+ # If the file was temporary, delete it
146
+ if (lContentFormat == CONTENT_LOCALFILENAME_TEMPORARY)
147
+ File.unlink(lLocalFileName)
148
+ end
149
+ end
150
+
151
+ return rError
152
+ end
153
+
154
+ # Get the URL handler corresponding to this URL
155
+ #
156
+ # Parameters:
157
+ # * *iURL* (_String_): The URL
158
+ # Return:
159
+ # * _Object_: The URL handler
160
+ def getURLHandler(iURL)
161
+ rURLHandler = nil
162
+
163
+ # Try out every regexp unless it matches.
164
+ # If none matches, assume a local file.
165
+ @Plugins.each do |iPluginName, iPluginInfo|
166
+ iRegexps, iPluginClassName = iPluginInfo
167
+ iRegexps.each do |iRegexp|
168
+ if (iRegexp.match(iURL) != nil)
169
+ # Found a matching handler
170
+ rURLHandler = eval("#{iPluginClassName}.new(iURL)")
171
+ break
172
+ end
173
+ end
174
+ if (rURLHandler != nil)
175
+ break
176
+ end
177
+ end
178
+ if (rURLHandler == nil)
179
+ # Assume a local file
180
+ rURLHandler = eval("#{@Plugins['LocalFile'][1]}.new(iURL)")
181
+ end
182
+
183
+ return rURLHandler
184
+ end
185
+
186
+ end
187
+
188
+ # Initialize a global plugins cache
189
+ def self.initializeURLAccess
190
+ $rUtilAnts_URLAccess_Manager = Manager.new
191
+ Object.module_eval('include RUtilAnts::URLAccess')
192
+ end
193
+
194
+ # Access the content of a URL.
195
+ # No cache.
196
+ # It calls a code block with the binary content of the URL (or a local file name if required).
197
+ #
198
+ # Parameters:
199
+ # * *iURL* (_String_): The URL (used to detect cyclic redirections)
200
+ # * *iParameters* (<em>map<Symbol,Object></em>): Additional parameters:
201
+ # ** *:FollowRedirections* (_Boolean_): Do we follow redirections ? [optional = true]
202
+ # ** *:NbrRedirectionsAllowed* (_Integer_): Number of redirections allowed [optional = 10]
203
+ # ** *:LocalFileAccess* (_Boolean_): Do we need a local file to read the content from ? If not, the content itslef will be given the code block. [optional = false]
204
+ # ** *:URLHandler* (_Object_): The URL handler, if it has already been instantiated, or nil otherwise [optional = nil]
205
+ # * _CodeBlock_: The code returning the object corresponding to the content:
206
+ # ** *iContent* (_String_): File content, or file name if :LocalFileAccess was true
207
+ # ** *iFileBaseName* (_String_): The base name the file could have. Useful to get file name extensions.
208
+ # ** Returns:
209
+ # ** _Exception_: The error encountered, or nil in case of success
210
+ def accessFile(iURL, iParameters = {})
211
+ return $rUtilAnts_URLAccess_Manager.accessFile(iURL, iParameters) do |iContent, iBaseName|
212
+ yield(iContent, iBaseName)
213
+ end
214
+ end
215
+
216
+ # Get the URL handler corresponding to this URL
217
+ #
218
+ # Parameters:
219
+ # * *iURL* (_String_): The URL
220
+ # Return:
221
+ # * _Object_: The URL handler
222
+ def getURLHandler(iURL)
223
+ return $rUtilAnts_URLAccess_Manager.getURLHandler(iURL)
224
+ end
225
+
226
+ end
227
+
228
+ end
@@ -0,0 +1,145 @@
1
+ #--
2
+ # Copyright (c) 2009 Muriel Salvan (murielsalvan@users.sourceforge.net)
3
+ # Licensed under the terms specified in LICENSE file. No warranty is provided.
4
+ #++
5
+
6
+ module RUtilAnts
7
+
8
+ module URLCache
9
+
10
+ # Class that caches every access to a URI (local file name, http, data...).
11
+ # This ensures just that several files are instantiated just once.
12
+ # For local files, it takes into account the file modification date/time to know if the Wx::Bitmap file has to be refreshed.
13
+ class URLCache
14
+
15
+ # Exception for reporting server down errors.
16
+ class ServerDownError < RuntimeError
17
+ end
18
+
19
+ # Constructor
20
+ def initialize
21
+ # Map of known contents, interpreted in many flavors
22
+ # map< Integer, [ Integer, Object ] >
23
+ # map< URL's hash, [ CRC, Content ] >
24
+ @URLs = {}
25
+ # Map of hosts down (no need to try again such a host)
26
+ # map< String >
27
+ @HostsDown = {}
28
+ end
29
+
30
+ # Get a content from a URL.
31
+ # Here are the different formats the URL can have:
32
+ # * Local file name
33
+ # * http/https/ftp/ftps:// protocols
34
+ # * data:image URI
35
+ # * file:// protocol
36
+ # It also handles redirections or zipped files
37
+ #
38
+ # Parameters:
39
+ # * *iURL* (_String_): The URL
40
+ # * *iParameters* (<em>map<Symbol,Object></em>): Additional parameters:
41
+ # ** *:ForceLoad* (_Boolean_): Do we force to refresh the cache ? [optional = false]
42
+ # ** *:FollowRedirections* (_Boolean_): Do we follow redirections ? [optional = true]
43
+ # ** *:NbrRedirectionsAllowed* (_Integer_): Number of redirections allowed [optional = 10]
44
+ # ** *:LocalFileAccess* (_Boolean_): Do we need a local file to read the content from ? If not, the content itslef will be given the code block. [optional = false]
45
+ # * _CodeBlock_: The code returning the object corresponding to the content:
46
+ # ** *iContent* (_String_): File content, or file name if :LocalFileAccess was true
47
+ # ** Returns:
48
+ # ** _Object_: Object read from the content, or nil in case of error
49
+ # ** _Exception_: The error encountered, or nil in case of success
50
+ # Return:
51
+ # * <em>Object</em>: The corresponding URL content, or nil in case of failure
52
+ # * _Exception_: The error, or nil in case of success
53
+ def getURLContent(iURL, iParameters = {})
54
+ rObject = nil
55
+ rError = nil
56
+
57
+ # Parse parameters
58
+ lForceLoad = iParameters[:ForceLoad]
59
+ if (lForceLoad == nil)
60
+ lForceLoad = false
61
+ end
62
+ # Get the URL handler corresponding to this URL
63
+ lURLHandler = getURLHandler(iURL)
64
+ lServerID = lURLHandler.getServerID
65
+ if (@HostsDown.has_key?(lServerID))
66
+ rError = ServerDownError.new("Server #{iURL} is currently down.")
67
+ else
68
+ lURLHash = iURL.hash
69
+ # Check if it is in the cache, or if we force refresh, or if the URL was invalidated
70
+ lCurrentCRC = lURLHandler.getCRC
71
+ if ((@URLs[lURLHash] == nil) or
72
+ (lForceLoad) or
73
+ (@URLs[lURLHash][0] != lCurrentCRC))
74
+ # Load it for real
75
+ # Reset previous value if it was set
76
+ @URLs[lURLHash] = nil
77
+ # Get the object
78
+ lObject = nil
79
+ lAccessError = accessFile(iURL, iParameters.merge(:URLHandler => lURLHandler)) do |iContent, iBaseName|
80
+ lObject, rError = yield(iContent)
81
+ end
82
+ if (lAccessError != nil)
83
+ rError = lAccessError
84
+ end
85
+ # Put lObject in the cache if no error was found
86
+ if (rError == nil)
87
+ # OK, register it
88
+ @URLs[lURLHash] = [ lCurrentCRC, lObject ]
89
+ else
90
+ if ((defined?(SocketError) != nil) and
91
+ (rError.is_a?(SocketError)))
92
+ # We have a server down
93
+ @HostsDown[lServerID] = nil
94
+ end
95
+ end
96
+ end
97
+ # If no error was found (errors can only happen if it was not already in the cache), take it from the cache
98
+ if (rError == nil)
99
+ rObject = @URLs[lURLHash][1]
100
+ end
101
+ end
102
+
103
+ return rObject, rError
104
+ end
105
+
106
+ end
107
+
108
+ # Initialize a global cache
109
+ def self.initializeURLCache
110
+ $rUtilAnts_URLCache = URLCache.new
111
+ Object.module_eval('include RUtilAnts::URLCache')
112
+ end
113
+
114
+ # Get a content from a URL.
115
+ # Here are the different formats the URL can have:
116
+ # * Local file name
117
+ # * http/https/ftp/ftps:// protocols
118
+ # * data:image URI
119
+ # * file:// protocol
120
+ # It also handles redirections or zipped files
121
+ #
122
+ # Parameters:
123
+ # * *iURL* (_String_): The URL
124
+ # * *iParameters* (<em>map<Symbol,Object></em>): Additional parameters:
125
+ # ** *:ForceLoad* (_Boolean_): Do we force to refresh the cache ? [optional = false]
126
+ # ** *:FollowRedirections* (_Boolean_): Do we follow redirections ? [optional = true]
127
+ # ** *:NbrRedirectionsAllowed* (_Integer_): Number of redirections allowed [optional = 10]
128
+ # ** *:LocalFileAccess* (_Boolean_): Do we need a local file to read the content from ? If not, the content itself will be given the code block. [optional = false]
129
+ # * _CodeBlock_: The code returning the object corresponding to the content:
130
+ # ** *iContent* (_String_): File content, or file name if :LocalFileAccess was true
131
+ # ** Returns:
132
+ # ** _Object_: Object read from the content, or nil in case of error
133
+ # ** _Exception_: The error encountered, or nil in case of success
134
+ # Return:
135
+ # * <em>Object</em>: The corresponding URL content, or nil in case of failure
136
+ # * _Exception_: The error, or nil in case of success
137
+ def getURLContent(iURL, iParameters = {})
138
+ return $rUtilAnts_URLCache.getURLContent(iURL, iParameters) do |iContent|
139
+ next yield(iContent)
140
+ end
141
+ end
142
+
143
+ end
144
+
145
+ end
@@ -0,0 +1,104 @@
1
+ #--
2
+ # Copyright (c) 2009 Muriel Salvan (murielsalvan@users.sourceforge.net)
3
+ # Licensed under the terms specified in LICENSE file. No warranty is provided.
4
+ #++
5
+
6
+ module RUtilAnts
7
+
8
+ module URLCache
9
+
10
+ module URLHandlers
11
+
12
+ # Handler of data:image URIs
13
+ class DataImage
14
+
15
+ # Get a list of regexps matching the URL to get to this handler
16
+ #
17
+ # Return:
18
+ # * <em>list<Regexp></em>: The list of regexps matching URLs from this handler
19
+ def self.getMatchingRegexps
20
+ return [
21
+ /^data:image.*$/
22
+ ]
23
+ end
24
+
25
+ # Constructor
26
+ #
27
+ # Parameters:
28
+ # * *iURL* (_String_): The URL that this handler will manage
29
+ def initialize(iURL)
30
+ @URL = iURL
31
+ lMatchData = @URL.match(/data:image\/(.*);base64,(.*)/)
32
+ if (lMatchData == nil)
33
+ logBug "URL #{iURL[0..23]}... was identified as a data:image like, but it appears to be false."
34
+ else
35
+ @Ext = lMatchData[1]
36
+ if (@Ext == 'x-icon')
37
+ @Ext = 'ico'
38
+ end
39
+ @Data = lMatchData[2]
40
+ end
41
+ end
42
+
43
+ # Get the server ID
44
+ #
45
+ # Return:
46
+ # * _String_: The server ID
47
+ def getServerID
48
+ return nil
49
+ end
50
+
51
+ # Get the current CRC of the URL
52
+ #
53
+ # Return:
54
+ # * _Integer_: The CRC
55
+ def getCRC
56
+ # As the content is in the URL, it will be natural to not find it anymore in the cache when it is changed.
57
+ # Therefore there is no need to return a CRC.
58
+ return 0
59
+ end
60
+
61
+ # Get a corresponding file base name.
62
+ # This method has to make sure file extensions are respected, as it can be used for further processing.
63
+ #
64
+ # Return:
65
+ # * _String_: The file name
66
+ def getCorrespondingFileBaseName
67
+ return "DataImage.#{@Ext}"
68
+ end
69
+
70
+ # Get the content of the URL
71
+ #
72
+ # Parameters:
73
+ # * *iFollowRedirections* (_Boolean_): Do we follow redirections while accessing the content ?
74
+ # Return:
75
+ # * _Integer_: Type of content returned
76
+ # * _Object_: The content, depending on the type previously returned:
77
+ # ** _Exception_ if CONTENT_ERROR: The corresponding error
78
+ # ** _String_ if CONTENT_REDIRECT: The new URL
79
+ # ** _String_ if CONTENT_STRING: The real content
80
+ # ** _String_ if CONTENT_LOCALFILENAME: The name of the local file name storing the content
81
+ # ** _String_ if CONTENT_LOCALFILENAME_TEMPORARY: The name of the temporary local file name storing the content
82
+ def getContent(iFollowRedirections)
83
+ rContentFormat = nil
84
+ rContent = nil
85
+
86
+ # Here we unpack the string in a base64 encoding.
87
+ if (@Data.empty?)
88
+ rContent = RuntimeError.new("Empty URI to decode: #{@URL}")
89
+ rContentFormat = CONTENT_ERROR
90
+ else
91
+ rContent = @Data.unpack('m')[0]
92
+ rContentFormat = CONTENT_STRING
93
+ end
94
+
95
+ return rContentFormat, rContent
96
+ end
97
+
98
+ end
99
+
100
+ end
101
+
102
+ end
103
+
104
+ end
@@ -0,0 +1,120 @@
1
+ #--
2
+ # Copyright (c) 2009 Muriel Salvan (murielsalvan@users.sourceforge.net)
3
+ # Licensed under the terms specified in LICENSE file. No warranty is provided.
4
+ #++
5
+
6
+ module RUtilAnts
7
+
8
+ module URLCache
9
+
10
+ module URLHandlers
11
+
12
+ # Handler of FTP URLs
13
+ class FTP
14
+
15
+ # Get a list of regexps matching the URL to get to this handler
16
+ #
17
+ # Return:
18
+ # * <em>list<Regexp></em>: The list of regexps matching URLs from this handler
19
+ def self.getMatchingRegexps
20
+ return [
21
+ /^(ftp|ftps):\/\/.*$/
22
+ ]
23
+ end
24
+
25
+ # Constructor
26
+ #
27
+ # Parameters:
28
+ # * *iURL* (_String_): The URL that this handler will manage
29
+ def initialize(iURL)
30
+ @URL = iURL
31
+ lURLMatch = iURL.match(/^(ftp|ftps):\/\/([^\/]*)\/(.*)$/)
32
+ if (lURLMatch == nil)
33
+ lURLMatch = iURL.match(/^(ftp|ftps):\/\/(.*)$/)
34
+ end
35
+ if (lURLMatch == nil)
36
+ logBug "URL #{iURL} was identified as an ftp like, but it appears to be false."
37
+ else
38
+ @URLProtocol, @URLServer, @URLPath = lURLMatch[1..3]
39
+ end
40
+ end
41
+
42
+ # Get the server ID
43
+ #
44
+ # Return:
45
+ # * _String_: The server ID
46
+ def getServerID
47
+ return "#{@URLProtocol}://#{@URLServer}"
48
+ end
49
+
50
+ # Get the current CRC of the URL
51
+ #
52
+ # Return:
53
+ # * _Integer_: The CRC
54
+ def getCRC
55
+ # We consider FTP URLs to be definitive: CRCs will never change.
56
+ return 0
57
+ end
58
+
59
+ # Get a corresponding file base name.
60
+ # This method has to make sure file extensions are respected, as it can be used for further processing.
61
+ #
62
+ # Return:
63
+ # * _String_: The file name
64
+ def getCorrespondingFileBaseName
65
+ lBase = File.basename(@URLPath)
66
+ lExt = File.extname(@URLPath)
67
+ lFileName = nil
68
+ if (lExt.empty?)
69
+ lFileName = lBase
70
+ else
71
+ # Check that extension has no characters following the URL (#, ? and ;)
72
+ lBase = lBase[0..lBase.size-lExt.size-1]
73
+ lFileName = "#{lBase}#{lExt.gsub(/^([^#\?;]*).*$/,'\1')}"
74
+ end
75
+
76
+ return getValidFileName(lFileName)
77
+ end
78
+
79
+ # Get the content of the URL
80
+ #
81
+ # Parameters:
82
+ # * *iFollowRedirections* (_Boolean_): Do we follow redirections while accessing the content ?
83
+ # Return:
84
+ # * _Integer_: Type of content returned
85
+ # * _Object_: The content, depending on the type previously returned:
86
+ # ** _Exception_ if CONTENT_ERROR: The corresponding error
87
+ # ** _String_ if CONTENT_REDIRECT: The new URL
88
+ # ** _String_ if CONTENT_STRING: The real content
89
+ # ** _String_ if CONTENT_LOCALFILENAME: The name of the local file name storing the content
90
+ # ** _String_ if CONTENT_LOCALFILENAME_TEMPORARY: The name of the temporary local file name storing the content
91
+ def getContent(iFollowRedirections)
92
+ rContentFormat = nil
93
+ rContent = nil
94
+
95
+ begin
96
+ require 'net/ftp'
97
+ lFTPConnection = Net::FTP.new(@URLServer)
98
+ lFTPConnection.login
99
+ lFTPConnection.chdir(File.dirname(@URLPath))
100
+ rContent = getCorrespondingFileBaseName
101
+ rContentFormat = CONTENT_LOCALFILENAME_TEMPORARY
102
+ logDebug "URL #{@URL} => Temporary file #{rContent}"
103
+ lFTPConnection.getbinaryfile(File.basename(@URLPath), rContent)
104
+ lFTPConnection.close
105
+ rescue Exception
106
+ rContent = $!
107
+ rContentFormat = CONTENT_ERROR
108
+ logDebug "Error accessing #{@URL}: #{rContent}"
109
+ end
110
+
111
+ return rContentFormat, rContent
112
+ end
113
+
114
+ end
115
+
116
+ end
117
+
118
+ end
119
+
120
+ end