StatsCollect 0.1.1.20101220 → 0.2.0.20110830

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/ChangeLog CHANGED
@@ -1,5 +1,103 @@
1
1
  = StatsCollect Release History
2
2
 
3
+ == 0.2.0.20110830 (Beta)
4
+
5
+ === General
6
+
7
+ * Migrated TODO list to Trackers.
8
+ * Updated copyright information.
9
+ * Migrated source repository to git.
10
+ * Used fileMutex from last rUtilAnts version.
11
+ * Added useful log info.
12
+ * Completed the conf example file.
13
+ * New API: Location plugins can now add a complete list of stats at once.
14
+ * New API: Backends have to be able to return a given stat.
15
+ * When stats are added with a given timestamp, their existence will be checked before add.
16
+ * Added RB location plugin to add stats from a Ruby list defined in a file.
17
+ * Collection plugins can now add stats for a given timestamp.
18
+ * Collection plugins can now add stats for a location different than they.
19
+ * Collection plugins can now ask for the list of known categories, locations and objects.
20
+ * Removed the logging of errors details a second time at the end of logs.
21
+ * Added CSV plugin to collect stats from CSV files.
22
+ * Added a string type value
23
+ * Added a way to recap all encountered errors at the end of the log.
24
+ * Added PID info in logs.
25
+ * Lock file is removed when the process it monitors has disappeared.
26
+ * Added MAP stats value type.
27
+ * Added new error logging: no more needed to parse the whole logs to check for errors presence.
28
+ * Changed API for Backends: Implemented a simplified and more flexible way to specify Stats Orders.
29
+ * Add a configurable delay when re-enqueuing recoverable stats orders.
30
+ * Added a closeSession method to finalize Backend sessions.
31
+ * Bug correction: StatsCollect did not execute due to new rUtilAnts lib.
32
+ * Bug correction: Lock file was not deleted when exception occurred.
33
+
34
+ === Backends/MySQL
35
+
36
+ * Uses MySQL connection and prepared statements pools
37
+ * Implemented a way to store Ruby maps as values in a differential storage (encode differences only).
38
+ * Process only stats orders with past times tamps
39
+ * Adapted to the new API.
40
+ * Now uses prepared queries for performance and security.
41
+ * Bug correction: Stats orders were not read correctly from DB.
42
+
43
+ === Backends/Terminal
44
+
45
+ * Adapted to the new API.
46
+ * Bug correction: Enqueued order was invalid.
47
+
48
+ === Locations/Facebook
49
+
50
+ * Updated to the new URL scheme. Configuration files need to add :URLID attribute.
51
+ * Facebook changed pages code.
52
+ * Changed the way number of friends were retrieved, as Facebook code changed
53
+ * Changed UserAgent as Facebook considered it as mobile
54
+
55
+ === Locations/FacebookArtist
56
+
57
+ * Facebook changed pages code.
58
+ * Changed UserAgent as Facebook considered it as mobile
59
+
60
+ === Locations/GoogleGroup
61
+
62
+ * Added compatibility between Mechanize 1.0.0 and 2.0.1.
63
+ * Updated to Mechanize 2.0.1 API.
64
+ * Added a new Location plugin to parse GoogleGroups.
65
+
66
+ === Locations/MySpace
67
+
68
+ * Corrected MySpace code as it has changed.
69
+ * Changed the way comments are parsed.
70
+ * Added a stat to get complete Friends list with IDs and names.
71
+ * Bug correction: Visits and Friends where not counted correctly when exceeding 1000.
72
+ * Bug correction: Friends lists did not match names on IDs sometimes.
73
+
74
+ === Locations/ReverbNation
75
+
76
+ * Changed way to parse songs and videos plays
77
+ * Changed way to parse integers with thousand separators.
78
+
79
+ === Locations/Youtube
80
+
81
+ * Added compatibility between Mechanize 1.0.0 and 2.0.1.
82
+ * Updated to Mechanize 2.0.1 API.
83
+ * Adapted to new code.
84
+ * Bug correction: Counters were not parsed correctly when greater than 1000.
85
+
86
+ === Notifiers/Custom
87
+
88
+ * Added a customizable notifier
89
+
90
+ === Notifiers/LogFile
91
+
92
+ * Added a log file notifier
93
+
94
+ == 0.1.1.20101220 (Beta)
95
+
96
+ * Facebook: Adapted code to new profiles.
97
+ * Youtube: Added Following, Followers, Visits and Friends stats.
98
+ * Added a way to push a new stats order using the API.
99
+ * Bug correction: MySQL: Did not work.
100
+
3
101
  == 0.1.0.20101220 (Beta)
4
102
 
5
103
  * Initial version
data/LICENSE CHANGED
@@ -6,7 +6,7 @@ This list is found in the file named AUTHORS.
6
6
  The AUTHORS and LICENSE files have to be included in any release of software
7
7
  embedding source code of this package, or using it as a derivative software.
8
8
 
9
- Copyright (c) 2009-2010 Muriel Salvan (murielsalvan@users.sourceforge.net)
9
+ Copyright (c) 2010 - 2011 Muriel Salvan (murielsalvan@users.sourceforge.net)
10
10
 
11
11
  Redistribution and use in source and binary forms, with or without
12
12
  modification, are permitted provided that the following conditions are met:
data/ReleaseInfo CHANGED
@@ -1,8 +1,8 @@
1
-
2
- # This file has been generated by RubyPackager during a delivery.
3
- # More info about RubyPackager: http://rubypackager.sourceforge.net
4
- {
5
- :Version => '0.1.1.20101220',
6
- :Tags => [ 'Beta' ],
7
- :DevStatus => 'Beta'
8
- }
1
+
2
+ # This file has been generated by RubyPackager during a delivery.
3
+ # More info about RubyPackager: http://rubypackager.sourceforge.net
4
+ {
5
+ :Version => '0.2.0.20110830',
6
+ :Tags => [ 'Beta' ],
7
+ :DevStatus => 'Beta'
8
+ }
@@ -1,4 +1,7 @@
1
1
  {
2
+ # Delay to apply before retrying recoverable stats orders (number of seconds)
3
+ :RecoverableErrorsRetryDelay => 300,
4
+
2
5
  # Configuration for Backends
3
6
  :Backends => {
4
7
  'MySQL' => {
@@ -27,7 +30,16 @@
27
30
  # To who the notifications are sent
28
31
  :To => 'mail@host.com'
29
32
  },
30
- 'None' => {}
33
+ 'None' => {},
34
+ 'LogFile' => {
35
+ :FileName => '/log/MyLogFile.log',
36
+ :Append => true
37
+ },
38
+ 'Custom' => {
39
+ :SendCode => Proc.new do |iMessage|
40
+ puts iMessage
41
+ end
42
+ }
31
43
  },
32
44
 
33
45
  # Configuration for Locations
@@ -46,7 +58,8 @@
46
58
  },
47
59
  'Facebook' => {
48
60
  :LoginEMail => 'FacebookLogin',
49
- :LoginPassword => '*****'
61
+ :LoginPassword => '*****',
62
+ :URLID => 'facebookid'
50
63
  },
51
64
  'FacebookArtist' => {
52
65
  :LoginEMail => 'FacebookLogin',
@@ -66,6 +79,10 @@
66
79
  'www.myhost.com'
67
80
  ]
68
81
  },
82
+ 'Youtube' => {
83
+ :LoginEMail => 'YoutubeLogin',
84
+ :LoginPassword => '*****'
85
+ },
69
86
  'FacebookLike' => {
70
87
  # List of objects for which we retrieve the Facebook likes
71
88
  :Objects => [
@@ -87,5 +104,29 @@
87
104
  'GoogleSearchString'
88
105
  ]
89
106
  },
107
+ 'GoogleGroup' => {
108
+ :LoginEMail => 'GoogleGroupLogin',
109
+ :LoginPassword => '*****',
110
+ :Objects => [
111
+ 'GoogleGroupName'
112
+ ]
113
+ },
114
+ 'CSV' => {
115
+ :ColumnSeparator => ',',
116
+ :RowSeparator => "\n",
117
+ :QuoteChar => '"',
118
+ :DateTimeFormat => '%d/%m/%y %H:%M',
119
+ :IDsMustExist => false,
120
+ :Files => [
121
+ 'C:\\Temp\\Stats.csv'
122
+ ]
123
+ },
124
+ 'RB' => {
125
+ :DateTimeFormat => '%Y-%m-%d %H:%M:%S',
126
+ :IDsMustExist => false,
127
+ :Files => [
128
+ 'C:\\Temp\\Stats.rb'
129
+ ]
130
+ }
90
131
  }
91
132
  }
data/bin/StatsCollect.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/bin/env ruby
2
2
  #--
3
- # Copyright (c) 2009-2010 Muriel Salvan (murielsalvan@users.sourceforge.net)
3
+ # Copyright (c) 2010 - 2011 Muriel Salvan (murielsalvan@users.sourceforge.net)
4
4
  # Licensed under the terms specified in LICENSE file. No warranty is provided.
5
5
  #++
6
6
 
@@ -1,56 +1,100 @@
1
1
  #--
2
- # Copyright (c) 2009-2010 Muriel Salvan (murielsalvan@users.sourceforge.net)
2
+ # Copyright (c) 2010 - 2011 Muriel Salvan (murielsalvan@users.sourceforge.net)
3
3
  # Licensed under the terms specified in LICENSE file. No warranty is provided.
4
4
  #++
5
5
 
6
+ require 'zlib'
7
+
8
+ class DateTime
9
+
10
+ # Convert this date time to a MySQL time
11
+ def to_MySQLTime
12
+ return Mysql::Time.new(
13
+ self.year,
14
+ self.month,
15
+ self.day,
16
+ self.hour,
17
+ self.min,
18
+ self.sec)
19
+ end
20
+
21
+ end
22
+
6
23
  module StatsCollect
7
24
 
8
25
  module Backends
9
26
 
10
27
  class MySQL
11
28
 
29
+ # Constants used to represent differencial data storage
30
+ DIFFDATA_KEY = 'K'
31
+ DIFFDATA_MERGE = 'M'
32
+ DIFFDATA_DELETE = 'D'
33
+ DIFFDATA_MODIFY = 'O'
34
+ DIFFDATA_SAME = 'S'
35
+ # Number of maximal rows to store data differences before creating a new data key
36
+ DIFFDATA_MAX_NUMBER_OF_ROWS = 20
37
+
12
38
  # Initialize a session of this backend
13
39
  #
14
40
  # Parameters:
15
41
  # * *iConf* (<em>map<Symbol,Object></em>): Configuration of this backend
16
42
  def initSession(iConf)
17
- require 'mysql'
18
- @MySQLConnection = Mysql.new(iConf[:DBHost], iConf[:DBUser], iConf[:DBPassword], iConf[:DBName])
19
- # TODO: Use bound variables everywhere !
43
+ require 'rUtilAnts/MySQLPool'
44
+ RUtilAnts::MySQLPool::initializeMySQLPool
45
+ lError, @MySQLConnection = connectToMySQL(iConf[:DBHost], iConf[:DBName], iConf[:DBUser], iConf[:DBPassword])
46
+ if (lError != nil)
47
+ raise lError
48
+ end
49
+ @StatementSelectFromStatsOrders = getPreparedStatement(@MySQLConnection, 'SELECT id, timestamp, locations_list, objects_list, categories_list, status FROM stats_orders WHERE (status = 0 OR status = 1) AND timestamp < ? ORDER BY timestamp DESC', :LeaveOpen => true)
50
+ @StatementDeleteFromStatsOrders = getPreparedStatement(@MySQLConnection, 'DELETE FROM stats_orders WHERE id=?', :LeaveOpen => true)
51
+ @StatementInsertIntoStatsLocations = getPreparedStatement(@MySQLConnection, 'INSERT INTO stats_locations (name) VALUES (?)', :LeaveOpen => true)
52
+ @StatementInsertIntoStatsCategories = getPreparedStatement(@MySQLConnection, 'INSERT INTO stats_categories (name, value_type) VALUES (?, ?)', :LeaveOpen => true)
53
+ @StatementInsertIntoStatsObjects = getPreparedStatement(@MySQLConnection, 'INSERT INTO stats_objects (name) VALUES (?)', :LeaveOpen => true)
54
+ @StatementInsertIntoStatsValues = getPreparedStatement(@MySQLConnection, 'INSERT INTO stats_values (timestamp, stats_location_id, stats_object_id, stats_category_id, value) VALUES (?, ?, ?, ?, ?)', :LeaveOpen => true)
55
+ @StatementSelectFromStatsValues = getPreparedStatement(@MySQLConnection, 'SELECT value FROM stats_values WHERE timestamp = ? AND stats_location_id = ? AND stats_object_id = ? AND stats_category_id = ?', :LeaveOpen => true)
56
+ @StatementInsertIntoStatsBinaryValues = getPreparedStatement(@MySQLConnection, 'INSERT INTO stats_binary_values (timestamp, stats_location_id, stats_object_id, stats_category_id, value) VALUES (?, ?, ?, ?, ?)', :LeaveOpen => true)
57
+ @StatementInsertIntoStatsOrders = getPreparedStatement(@MySQLConnection, 'INSERT INTO stats_orders (timestamp, locations_list, objects_list, categories_list, status) VALUES (?, ?, ?, ?, ?)', :LeaveOpen => true)
58
+ @StatementSelectFromStatsLastKeys = getPreparedStatement(@MySQLConnection, 'SELECT stats_value_id FROM stats_last_keys WHERE stats_location_id = ? AND stats_object_id = ? AND stats_category_id = ?', :LeaveOpen => true)
59
+ @StatementSelectFromStatsBinaryValues = getPreparedStatement(@MySQLConnection, 'SELECT id, value FROM stats_binary_values WHERE stats_location_id = ? AND stats_object_id = ? AND stats_category_id = ? AND id >= ? ORDER BY id', :LeaveOpen => true)
60
+ @StatementInsertIntoStatsLastKeys = getPreparedStatement(@MySQLConnection, 'INSERT INTO stats_last_keys (stats_location_id, stats_object_id, stats_category_id, stats_value_id) VALUES (?, ?, ?, ?)', :LeaveOpen => true)
61
+ @StatementUpdateStatsLastKeys = getPreparedStatement(@MySQLConnection, 'UPDATE stats_last_keys SET stats_value_id = ? WHERE stats_location_id = ? AND stats_object_id = ? AND stats_category_id = ?', :LeaveOpen => true)
20
62
  end
21
63
 
22
- # Get the next stats order.
23
- # Code to begin a new transaction can be set in this method too.
64
+ # Get the next stats orders.
24
65
  #
25
- # Return:
26
- # * _DateTime_: The time stamp, or nil if no new stats order
27
- # * <em>list<String></em>: List of locations
28
- # * <em>list<String></em>: List of objects
29
- # * <em>list<String></em>: List of categories
30
- # * _Integer_: The order status
31
- def getNextStatsOrder
32
- rTimeStamp = nil
33
- rLstLocations = nil
34
- rLstObjects = nil
35
- rLstCategories = nil
36
- rStatus = nil
37
-
38
- if (defined?(@LstStatsOrders) == nil)
39
- @LstStatsOrders = []
40
- @MySQLConnection.query('SELECT id, timestamp, objects_list, categories_list, locations_list, status FROM stats_orders WHERE status=0 OR status=1 ORDER BY timestamp DESC').each do |iRow|
41
- @LstStatsOrders << iRow.clone
42
- end
43
- end
44
- if (!@LstStatsOrders.empty?)
45
- lID, rTimeStamp, lStrLocations, lStrObjects, lStrCategories, rStatus = @LstStatsOrders.pop
46
- rLstLocations = lStrLocations.split('|')
47
- rLstObjects = lStrObjects.split('|')
48
- rLstCategories = lStrCategories.split('|')
49
- @MySQLConnection.query('start transaction')
50
- @MySQLConnection.query("DELETE FROM stats_orders WHERE id=#{lID}")
66
+ # Parameters:
67
+ # * *oStatsOrdersProxy* (_StatsOrdersProxy_): The stats orders proxy to be used to give stats orders
68
+ def getStatsOrders(oStatsOrdersProxy)
69
+ @StatementSelectFromStatsOrders.execute(DateTime.now.to_MySQLTime)
70
+ @StatementSelectFromStatsOrders.each do |iRow|
71
+ iID, iMySQLTimeStamp, iStrLocations, iStrObjects, iStrCategories, iStatus = iRow
72
+ oStatsOrdersProxy.addStatsOrder(
73
+ iID,
74
+ DateTime.civil(
75
+ iMySQLTimeStamp.year,
76
+ iMySQLTimeStamp.month,
77
+ iMySQLTimeStamp.day,
78
+ iMySQLTimeStamp.hour,
79
+ iMySQLTimeStamp.minute,
80
+ iMySQLTimeStamp.second),
81
+ iStrLocations.split('|'),
82
+ iStrObjects.split('|'),
83
+ iStrCategories.split('|'),
84
+ iStatus)
51
85
  end
86
+ end
52
87
 
53
- return rTimeStamp, rLstLocations, rLstObjects, rLstCategories, rStatus
88
+ # Dequeue the given stat orders IDs.
89
+ # Code to begin a new transaction can be set in this method too. In this case, the dequeue should be part of the transaction, or it will have to be re-enqueued during rollback method call (otherwise orders will be lost).
90
+ #
91
+ # Parameters:
92
+ # * *iLstStatsOrderIDs* (<em>list<Integer></em>): The list of stats order IDs to dequeue
93
+ def dequeueStatsOrders(iLstStatsOrderIDs)
94
+ @MySQLConnection.query('start transaction')
95
+ iLstStatsOrderIDs.each do |iStatsOrderID|
96
+ @StatementDeleteFromStatsOrders.execute(iStatsOrderID)
97
+ end
54
98
  end
55
99
 
56
100
  # Get the list of known locations
@@ -105,9 +149,9 @@ module StatsCollect
105
149
  # Return:
106
150
  # * _Integer_: Its resulting ID
107
151
  def addLocation(iLocation)
108
- @MySQLConnection.query("INSERT INTO stats_locations (name) VALUES ('#{@MySQLConnection.escape_string(iLocation)}')")
152
+ @StatementInsertIntoStatsLocations.execute(iLocation)
109
153
 
110
- return @MySQLConnection.insert_id
154
+ return @StatementInsertIntoStatsLocations.insert_id
111
155
  end
112
156
 
113
157
  # Add a new category
@@ -118,9 +162,9 @@ module StatsCollect
118
162
  # Return:
119
163
  # * _Integer_: Its resulting ID
120
164
  def addCategory(iCategory, iValueType)
121
- @MySQLConnection.query("INSERT INTO stats_categories (name, value_type) VALUES ('#{@MySQLConnection.escape_string(iCategory)}', #{iValueType})")
165
+ @StatementInsertIntoStatsCategories.execute(iCategory, iValueType)
122
166
 
123
- return @MySQLConnection.insert_id
167
+ return @StatementInsertIntoStatsCategories.insert_id
124
168
  end
125
169
 
126
170
  # Add a new object
@@ -130,9 +174,9 @@ module StatsCollect
130
174
  # Return:
131
175
  # * _Integer_: Its resulting ID
132
176
  def addObject(iObject)
133
- @MySQLConnection.query("INSERT INTO stats_objects (name) VALUES ('#{@MySQLConnection.escape_string(iObject)}')")
177
+ @StatementInsertIntoStatsObjects.execute(iObject)
134
178
 
135
- return @MySQLConnection.insert_id
179
+ return @StatementInsertIntoStatsObjects.insert_id
136
180
  end
137
181
 
138
182
  # Add a new stat
@@ -145,6 +189,10 @@ module StatsCollect
145
189
  # * *iValue* (_Object_): The value to store
146
190
  # * *iValueType* (_Integer_): The value type
147
191
  def addStat(iTimeStamp, iLocationID, iObjectID, iCategoryID, iValue, iValueType)
192
+ # Do we need to store this value ID in the last keys ?
193
+ lStoreInLastKeys = false
194
+ lExistingLastKey = false
195
+ lInsertStatement = @StatementInsertIntoStatsValues
148
196
  # Convert the value to its internal representation
149
197
  lStrValue = nil
150
198
  case iValueType
@@ -156,12 +204,151 @@ module StatsCollect
156
204
  lStrValue = iValue.to_s
157
205
  when STATS_VALUE_TYPE_UNKNOWN
158
206
  lStrValue = iValue.to_s
207
+ when STATS_VALUE_TYPE_MAP
208
+ lInsertStatement = @StatementInsertIntoStatsBinaryValues
209
+ # This is a special case:
210
+ # We retrieve the last value of this statistic, and decide if we write it completely, or just write a diff.
211
+ lLastKeyStatsValueID = nil
212
+ @StatementSelectFromStatsLastKeys.execute(iLocationID, iObjectID, iCategoryID)
213
+ # Should be only 1 row, or none
214
+ @StatementSelectFromStatsLastKeys.each do |iRow|
215
+ lLastKeyStatsValueID = iRow[0]
216
+ end
217
+ if (lLastKeyStatsValueID == nil)
218
+ # First value to store: store a key
219
+ lStrValue = "#{DIFFDATA_KEY}#{Zlib::Deflate.new.deflate(Marshal.dump(iValue), Zlib::FINISH)}"
220
+ lStoreInLastKeys = true
221
+ else
222
+ # There was already a previous key for this value.
223
+ lExistingLastKey = true
224
+ # Reconstruct the value by getting all the stats_values since this key.
225
+ lExistingValue = nil
226
+ @StatementSelectFromStatsBinaryValues.execute(iLocationID, iObjectID, iCategoryID, lLastKeyStatsValueID)
227
+ # If too much rows, we just create a new key
228
+ lNbrRows = @StatementSelectFromStatsBinaryValues.num_rows
229
+ if ((lNbrRows == 0) or
230
+ (lNbrRows >= DIFFDATA_MAX_NUMBER_OF_ROWS))
231
+ lStrValue = "#{DIFFDATA_KEY}#{Zlib::Deflate.new.deflate(Marshal.dump(iValue), Zlib::FINISH)}"
232
+ lStoreInLastKeys = true
233
+ else
234
+ @StatementSelectFromStatsBinaryValues.each do |iRow|
235
+ iID, iRowValue = iRow
236
+ # Read the type of this diff data
237
+ case iRowValue[0..0]
238
+ when DIFFDATA_KEY
239
+ lExistingValue = Marshal.load(Zlib::Inflate.new.inflate(iRowValue[1..-1]))
240
+ when DIFFDATA_MERGE
241
+ lExistingValue.merge!(Marshal.load(iRowValue[1..-1]))
242
+ when DIFFDATA_DELETE
243
+ lValuesToDelete = Marshal.load(iRowValue[1..-1])
244
+ lExistingValue.delete_if do |iKey, iExistingValue|
245
+ next (lValuesToDelete.include?(iKey))
246
+ end
247
+ when DIFFDATA_MODIFY
248
+ lValuesToDelete, lValuesToModify = Marshal.load(iRowValue[1..-1])
249
+ lExistingValue.delete_if do |iKey, iExistingValue|
250
+ next (lValuesToDelete.include?(iKey))
251
+ end
252
+ lExistingValue.merge!(lValuesToModify)
253
+ when DIFFDATA_SAME
254
+ # Nothing to do
255
+ else
256
+ logErr "Unknown diff value type: #{iRowValue[0..0]}"
257
+ raise RuntimeError.new("Unknown diff value type: #{iRowValue[0..0]}")
258
+ end
259
+ end
260
+ # Now compute the difference between the existing value and the new one
261
+ lValuesToDelete = []
262
+ lValuesToMerge = {}
263
+ iValue.each do |iKey, iNewValue|
264
+ if (lExistingValue.has_key?(iKey))
265
+ if (iNewValue != lExistingValue[iKey])
266
+ # A modified value: add it
267
+ lValuesToMerge[iKey] = iNewValue
268
+ end
269
+ else
270
+ # A new value: add it
271
+ lValuesToMerge[iKey] = iNewValue
272
+ end
273
+ end
274
+ lExistingValue.each do |iKey, iExistingValue|
275
+ if (!iValue.has_key?(iKey))
276
+ # A missing value: delete it
277
+ lValuesToDelete << iKey
278
+ end
279
+ end
280
+ if (lValuesToDelete.empty?)
281
+ if (lValuesToMerge.empty?)
282
+ lStrValue = DIFFDATA_SAME
283
+ else
284
+ lStrValue = "#{DIFFDATA_MERGE}#{Marshal.dump(lValuesToMerge)}"
285
+ end
286
+ elsif (lValuesToMerge.empty?)
287
+ lStrValue = "#{DIFFDATA_DELETE}#{Marshal.dump(lValuesToDelete)}"
288
+ else
289
+ lStrValue = "#{DIFFDATA_MODIFY}#{Marshal.dump([lValuesToDelete,lValuesToMerge])}"
290
+ end
291
+ end
292
+ end
293
+ when STATS_VALUE_TYPE_STRING
294
+ lStrValue = iValue
159
295
  else
160
296
  logErr "Unknown category value type: #{iValueType}. It will be treated as Unknown."
161
297
  lStrValue = iValue.to_s
162
298
  end
163
299
  # Add the new stat in the DB for real
164
- @MySQLConnection.query("INSERT INTO stats_values (timestamp, stats_object_id, stats_category_id, stats_location_id, value) VALUES ('#{iTimeStamp.strftime('%Y-%m-%d %H:%M:%S')}', #{iObjectID}, #{iCategoryID}, #{iLocationID}, '#{lStrValue}')")
300
+ lInsertStatement.execute(iTimeStamp.to_MySQLTime, iLocationID, iObjectID, iCategoryID, lStrValue)
301
+ # Store the last key idf needed
302
+ if (lStoreInLastKeys)
303
+ lNewStatValueID = lInsertStatement.insert_id
304
+ if (lExistingLastKey)
305
+ @StatementUpdateStatsLastKeys.execute(lNewStatValueID, iLocationID, iObjectID, iCategoryID)
306
+ else
307
+ @StatementInsertIntoStatsLastKeys.execute(iLocationID, iObjectID, iCategoryID, lNewStatValueID)
308
+ end
309
+ end
310
+ end
311
+
312
+ # Get an existing stat value
313
+ #
314
+ # Parameters:
315
+ # * *iTimeStamp* (_DateTime_): The timestamp
316
+ # * *iLocationID* (_Integer_): The location ID
317
+ # * *iObjectID* (_Integer_): The object ID
318
+ # * *iCategoryID* (_Integer_): The category ID
319
+ # * *iValueType* (_Integer_): The value type
320
+ # Return:
321
+ # * _Object_: The corresponding value, or nil if none
322
+ def getStat(iTimeStamp, iLocationID, iObjectID, iCategoryID, iValueType)
323
+ rValue = nil
324
+
325
+ @StatementSelectFromStatsValues.execute(iTimeStamp.to_MySQLTime, iLocationID, iObjectID, iCategoryID)
326
+ if (@StatementSelectFromStatsValues.num_rows > 0)
327
+ @StatementSelectFromStatsValues.each do |iRow|
328
+ lStrValue = iRow[0]
329
+ case iValueType
330
+ when STATS_VALUE_TYPE_INTEGER
331
+ rValue = Integer(lStrValue)
332
+ when STATS_VALUE_TYPE_FLOAT
333
+ rValue = Float(lStrValue)
334
+ when STATS_VALUE_TYPE_PERCENTAGE
335
+ rValue = Float(lStrValue)
336
+ when STATS_VALUE_TYPE_UNKNOWN
337
+ rValue = lStrValue
338
+ when STATS_VALUE_TYPE_MAP
339
+ # TODO
340
+ rValue = lStrValue
341
+ when STATS_VALUE_TYPE_STRING
342
+ rValue = lStrValue
343
+ else
344
+ logErr "Unknown category value type: #{iValueType}. It will be treated as Unknown."
345
+ rValue = lStrValue
346
+ end
347
+ break
348
+ end
349
+ end
350
+
351
+ return rValue
165
352
  end
166
353
 
167
354
  # Add a new stats order
@@ -173,7 +360,14 @@ module StatsCollect
173
360
  # * *iLstCategories* (<em>list<String></em>): List of categories
174
361
  # * *iStatus* (_Integer_): The order status
175
362
  def putNewStatsOrder(iTimeStamp, iLstLocations, iLstObjects, iLstCategories, iStatus)
176
- @MySQLConnection.query("INSERT INTO stats_orders (timestamp, locations_list, objects_list, categories_list, status) VALUES ('#{iTimeStamp.strftime('%Y-%m-%d %H:%M:%S')}', '#{@MySQLConnection.escape_string(iLstLocations.join('|'))}', '#{@MySQLConnection.escape_string(iLstObjects.join('|'))}', '#{@MySQLConnection.escape_string(iLstCategories.join('|'))}', #{iStatus})")
363
+ lMySQLTime = Mysql::Time.new(
364
+ iTimeStamp.year,
365
+ iTimeStamp.month,
366
+ iTimeStamp.day,
367
+ iTimeStamp.hour,
368
+ iTimeStamp.min,
369
+ iTimeStamp.sec)
370
+ @StatementInsertIntoStatsOrders.execute(lMySQLTime, iLstLocations.join('|'), iLstObjects.join('|'), iLstCategories.join('|'), iStatus)
177
371
  end
178
372
 
179
373
  # Commit the current stats order transaction
@@ -186,6 +380,24 @@ module StatsCollect
186
380
  @MySQLConnection.query('rollback')
187
381
  end
188
382
 
383
+ # Close a session of this backend
384
+ def closeSession
385
+ closePreparedStatement(@MySQLConnection, @StatementSelectFromStatsOrders)
386
+ closePreparedStatement(@MySQLConnection, @StatementDeleteFromStatsOrders)
387
+ closePreparedStatement(@MySQLConnection, @StatementInsertIntoStatsLocations)
388
+ closePreparedStatement(@MySQLConnection, @StatementInsertIntoStatsCategories)
389
+ closePreparedStatement(@MySQLConnection, @StatementInsertIntoStatsObjects)
390
+ closePreparedStatement(@MySQLConnection, @StatementInsertIntoStatsValues)
391
+ closePreparedStatement(@MySQLConnection, @StatementSelectFromStatsValues)
392
+ closePreparedStatement(@MySQLConnection, @StatementInsertIntoStatsBinaryValues)
393
+ closePreparedStatement(@MySQLConnection, @StatementInsertIntoStatsOrders)
394
+ closePreparedStatement(@MySQLConnection, @StatementSelectFromStatsLastKeys)
395
+ closePreparedStatement(@MySQLConnection, @StatementSelectFromStatsBinaryValues)
396
+ closePreparedStatement(@MySQLConnection, @StatementInsertIntoStatsLastKeys)
397
+ closePreparedStatement(@MySQLConnection, @StatementUpdateStatsLastKeys)
398
+ closeMySQL(@MySQLConnection)
399
+ end
400
+
189
401
  end
190
402
 
191
403
  end