rUtilAnts 0.1.0.20091014

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.
@@ -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