StatsCollect 0.1.1.20101220 → 0.2.0.20110830

Sign up to get free protection for your applications and to get access to all the features.
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