|
1 /* |
|
2 * Copyright (c) 2007-2009 Nokia Corporation and/or its subsidiary(-ies). |
|
3 * All rights reserved. |
|
4 * This component and the accompanying materials are made available |
|
5 * under the terms of "Eclipse Public License v1.0" |
|
6 * which accompanies this distribution, and is available |
|
7 * at the URL "http://www.eclipse.org/legal/epl-v10.html". |
|
8 * |
|
9 * Initial Contributors: |
|
10 * Nokia Corporation - initial contribution. |
|
11 * |
|
12 * Contributors: |
|
13 * |
|
14 * Description: |
|
15 * |
|
16 */ |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 /** |
|
22 @file |
|
23 @internalComponent |
|
24 @released |
|
25 */ |
|
26 |
|
27 |
|
28 #ifndef __PLTABLES_H__ |
|
29 #define __PLTABLES_H__ |
|
30 |
|
31 #include "cntsqlprovider.h" |
|
32 #include "persistencelayer.h" |
|
33 #include "clplcontactproperties.h" |
|
34 #include <cntdef.h> |
|
35 #include <cntitem.h> |
|
36 #include <cntfldst.h> |
|
37 |
|
38 #include <sqldb.h> |
|
39 #include <e32hashtab.h> |
|
40 #include <QList> |
|
41 #include <QStringList> |
|
42 |
|
43 class CPcsKeyMap; |
|
44 |
|
45 |
|
46 /** |
|
47 The CPplTableBase class forms the base class for all SQLite tables in the |
|
48 Persistence Layer. It implements default behaviour for some basic operations. |
|
49 |
|
50 It holds member variables for the database and SQL statements representing the |
|
51 4 CRUD operations. The ReadL function is virtual with a default implementation |
|
52 that does nothing so that it can be overridden in the derived classes, as |
|
53 necessary. This is to allow the read operation to be performed on all classes |
|
54 in the same manner, even when in cases where there is nothing to be read. In |
|
55 this way, all table classes can be treated the same. This is to facilitate the |
|
56 use of the composite design pattern. |
|
57 */ |
|
58 NONSHARABLE_CLASS(CPplTableBase) : public CBase |
|
59 { |
|
60 public: |
|
61 virtual void CreateInDbL(CContactItem& aItem) = 0; |
|
62 virtual void UpdateL(const CContactItem& aItem) = 0; |
|
63 virtual void DeleteL(const CContactItem& aItem, TBool& aLowDiskErrorOccurred) = 0; |
|
64 virtual void CreateTableL() = 0; |
|
65 }; |
|
66 |
|
67 |
|
68 /** |
|
69 The CPplContactTable class has a dual nature, caused by inheritance from two |
|
70 interfaces: MPplContactItemPersistor and MLplPersistenceBroker. |
|
71 |
|
72 MPplContactItemPersistor inheritance makes the CPplContactTable a normal table |
|
73 capable of perfoming five basic operations (Create, Read, Update, Delete and |
|
74 ChangeType) when called from the parent table. |
|
75 |
|
76 MLplPersistenceBroker is a public interface responsible for carrying out the |
|
77 basic operations. It is exposed to the users of Persistence Layer making the |
|
78 RPplContactTable an entry point for these operations. |
|
79 |
|
80 As a result ContactTable appears twice in the tree structure of tables: once as |
|
81 a root table (MLplPersistenceBroker inteface) and second time as a child of |
|
82 Email table (MPplContactItemPersistor inteface). |
|
83 |
|
84 The Contact table is responsible for saving header information for all the |
|
85 fields in a contact and the values of most of the fields. Four C/BLOB fields |
|
86 are used for this purpose. |
|
87 |
|
88 Header information (including label and content type of the fields) is stored in |
|
89 in the binary_fields_header and text_fields_header columns. |
|
90 |
|
91 The binary_fields column accomodates the binary values of the fields (such as |
|
92 pictures or sounds). |
|
93 |
|
94 All human readable text values go to the text_fields column. This is particularly |
|
95 useful for searching through all textual fields of contacts. |
|
96 */ |
|
97 NONSHARABLE_CLASS(CPplContactTable) : public CPplTableBase |
|
98 { |
|
99 public: |
|
100 class THintField |
|
101 { |
|
102 friend class CPplContactTable; |
|
103 public: |
|
104 THintField(); |
|
105 void UpdateHintL(const CContactItemField& aField, const RArray<TUid>& aCustFiltFields); |
|
106 |
|
107 |
|
108 TUint16 ExtHint(); |
|
109 TInt8 Hint(); |
|
110 private: |
|
111 THintField(TUint16 aExtHint, TUint8 aHint); |
|
112 |
|
113 private: |
|
114 TInt iValue; |
|
115 }; |
|
116 |
|
117 |
|
118 public: |
|
119 static CPplContactTable* NewL(RSqlDatabase& aDatabase, CLplContactProperties& aProperties); |
|
120 static CPplContactTable* NewLC(RSqlDatabase& aDatabase, CLplContactProperties& aProperties); |
|
121 void CreateInDbL(CContactItem& aItem); |
|
122 void CreateInDbL(CContactItem& aItem, TUint aSessionId); |
|
123 |
|
124 void UpdateL(const CContactItem& aItem); |
|
125 void DeleteL(const CContactItem& aItem, TBool& aLowDiskErrorOccurred); |
|
126 CContactItem* DeleteLC(TContactItemId aItemId, TBool& aLowDiskErrorOccurred); |
|
127 void CreateTableL(); |
|
128 |
|
129 void ChangeTypeL(TContactItemId aItemId, TUid aNewType); |
|
130 TInt NameFieldUid(const CContactItemField& nameField); |
|
131 |
|
132 TBool IsTableEmptyL(); |
|
133 CContactIdArray& CardTemplateIdsL(); |
|
134 TContactItemId OwnCardIdL(); |
|
135 void SetOwnCardIdL(TContactItemId aId, TBool aPersist = ETrue); |
|
136 |
|
137 ~CPplContactTable(); |
|
138 |
|
139 |
|
140 private: |
|
141 CPplContactTable(RSqlDatabase& aDatabase, CLplContactProperties& aProps); |
|
142 void ConstructL(); |
|
143 void WriteContactItemL(const CContactItem& aItem, TCntSqlStatement aType); |
|
144 void UpdateTemplateAccessCounterL(TContactItemId aTemplateRefId, TBool aIncrement); |
|
145 void GetTypeFlagFields(TInt aTypeFlags, TUid& aType, TUint& aAttributes, TUint& aHintFields); |
|
146 TInt GenerateTypeFlags(TUid aType, TUint aAttributes, TUint aHintFields); |
|
147 TUint NumDigits(TInt aNum); |
|
148 |
|
149 private: |
|
150 CLplContactProperties& iProperties; |
|
151 CCntSqlStatement* iInsertStmnt; |
|
152 CCntSqlStatement* iDeleteSelectStmnt; |
|
153 CCntSqlStatement* iAccessCountUpdateStmnt; |
|
154 CCntSqlStatement* iUpdateFlagsStmnt; |
|
155 CCntSqlStatement* iUpdateStmnt; |
|
156 CCntSqlStatement* iDeleteStmnt; |
|
157 RHashMap<TInt, TPtrC> iFieldMap; |
|
158 RSqlDatabase& iDatabase; |
|
159 |
|
160 CContactIdArray* iCardTemplateIds; |
|
161 TContactItemId iOwnCardId; |
|
162 }; |
|
163 |
|
164 /** |
|
165 The CPplCommAddrTable class is used to store all communication addresses in a |
|
166 contact item. This includes telephone/fax numbers, email addresses and SIP |
|
167 addresses. |
|
168 |
|
169 Email and SIP addresses are stored as-is but telephone/fax numbers are hashed |
|
170 in this table. This is because in the text field in the contact table, telephone |
|
171 numbers are stored as text. This provides great flexibility for the users as they |
|
172 can use any symbol in their telephone number fields, such as '+','(',')', spaces |
|
173 or even words like 'ext.' or 'emergency only'. However, this form of information |
|
174 storage present a big problem in the phone matching use case i.e. when the |
|
175 Contacts Model should quickly find a contact with the given phone number in it. |
|
176 |
|
177 This problem is solved with the hashing mechanism. Each time the user creates a |
|
178 new phone field or changes the existing one the hash value is calculated and |
|
179 saved in the phone table. The hash value is calculated with the help of the |
|
180 cntphone plugin, which parses the text containing the telephone number and tries |
|
181 to find all the informative digits and symbols. |
|
182 |
|
183 Originally the hash value covered only last seven digits of the phone number and |
|
184 the TInt column was enough to store the hash. Later the value was extended and |
|
185 now it is stored in two TInt columns to provide data backward compatibility with |
|
186 previous versions of the Contacts Model. |
|
187 |
|
188 The comm_addr table contains a private helper class TMatch that represents the |
|
189 hash value and an enumeration to specify type of communication address. |
|
190 */ |
|
191 NONSHARABLE_CLASS(CPplCommAddrTable) : public CPplTableBase |
|
192 { |
|
193 private: |
|
194 class TMatch |
|
195 { |
|
196 public: |
|
197 TMatch(); |
|
198 |
|
199 static TInt32 CreateHashL(const TDesC& aPhoneNumberString, TInt aMatchLength, TInt& aNumPhoneDigits); |
|
200 static void StripOutNonDigitChars(TDes& aText); |
|
201 static TInt32 PadOutPhoneMatchNumber(TInt32& aPhoneNumber,TInt aPadOutLength); |
|
202 static TBool Equals (const TMatch& aRMatch ,const TMatch& aLMatch); |
|
203 inline TBool operator==(const TMatch &aMatch) const; |
|
204 |
|
205 public: |
|
206 TInt32 iLowerSevenDigits; |
|
207 TInt32 iUpperDigits; |
|
208 TInt iNumLowerDigits; |
|
209 TInt iNumUpperDigits; |
|
210 }; |
|
211 |
|
212 public: |
|
213 // defines type of communication address. |
|
214 // !! the values these represent are persisted in the database !! |
|
215 // !! do not change the order -- add new ones to the bottom !! |
|
216 // !! changing these values could break data compatibility !! |
|
217 enum TCommAddrType |
|
218 { |
|
219 EPhoneNumber, |
|
220 EEmailAddress, |
|
221 ESipAddress |
|
222 }; |
|
223 |
|
224 public: |
|
225 static CPplCommAddrTable* NewL(RSqlDatabase& aDatabase, CLplContactProperties& aProperties); |
|
226 static CPplCommAddrTable* NewLC(RSqlDatabase& aDatabase, CLplContactProperties& aProperties); |
|
227 void CreateInDbL(CContactItem& aItem); |
|
228 void UpdateL(const CContactItem& aItem); |
|
229 void DeleteL(const CContactItem& aItem, TBool& aLowDiskErrorOccurred); |
|
230 void CreateTableL(); |
|
231 |
|
232 CContactIdArray* MatchPhoneNumberL(const TDesC& aNumber, const TInt aMatchLengthFromRight); |
|
233 CContactIdArray* BestMatchingPhoneNumberL(const TDesC& aNumber); |
|
234 CContactIdArray* MatchEmailAddressL(const TDesC& aEmailAddr); |
|
235 CContactIdArray* MatchSipAddressL(const TDesC& aSipAddr); |
|
236 |
|
237 ~CPplCommAddrTable(); |
|
238 |
|
239 private: |
|
240 void ConstructL(); |
|
241 CPplCommAddrTable(RSqlDatabase& aDatabase, CLplContactProperties& iProperties); |
|
242 void RemoveNonUpdatedAddrsL(RArray<TMatch>& aNewPhones, RArray<TPtrC>& aNewEmails, RArray<TPtrC>& aNewSips, |
|
243 RArray<TInt>& aFreeCommAddrIds, const TInt aItemId); |
|
244 void DoUpdateCommAddrsL(RArray<TMatch>& aNewPhones, RArray<TPtrC>& aNewEmails, RArray<TPtrC>& aNewSips, |
|
245 RArray<TInt>& aFreeCommAddrIds, const TInt aItemId); |
|
246 void DeleteSingleCommAddrL(TInt aCommAddrId, TBool& aLowDiskErrorOccurred); |
|
247 void DoPhoneNumWriteOpL(const CPplCommAddrTable::TMatch& aPhoneNum, TCntSqlStatement aType, TInt aCntId); |
|
248 void DoPhoneNumWriteOpL(const CPplCommAddrTable::TMatch& aPhoneNum, TCntSqlStatement aType, TInt aCntId, |
|
249 TInt aCommAddrId); |
|
250 void DoNonPhoneWriteOpL(const TDesC& aAddress, TCntSqlStatement aType, TInt aCntId, |
|
251 TCommAddrType aAddrType); |
|
252 void DoNonPhoneWriteOpL(const TDesC& aAddress, TCntSqlStatement aType, TInt aCntId, |
|
253 TCommAddrType aAddrType, TInt aCommAddrId); |
|
254 CContactIdArray* MatchNonPhoneAddrL(const TDesC& aCommAddr, TCommAddrType aAddrType); |
|
255 CPplCommAddrTable::TMatch CreatePaddedPhoneDigitsL(const TDesC& aNumber, const TInt aNumLowerDigits, |
|
256 const TInt aNumUpperDigits); |
|
257 CPplCommAddrTable::TMatch CreatePhoneMatchNumberL(const TDesC& aText, TInt aLowerMatchLength, |
|
258 TInt aUpperMatchLength); |
|
259 |
|
260 private: |
|
261 CLplContactProperties& iProperties; |
|
262 CCntSqlStatement* iInsertStmnt; |
|
263 CCntSqlStatement* iWholeSelectStmnt; |
|
264 CCntSqlStatement* iMatchSelectStmnt; |
|
265 CCntSqlStatement* iUpdateStmnt; |
|
266 CCntSqlStatement* iAllForItemDeleteStmnt; |
|
267 CCntSqlStatement* iSingleDeleteStmnt; |
|
268 RSqlDatabase& iDatabase; |
|
269 }; |
|
270 |
|
271 |
|
272 /** |
|
273 The CPplGroupsTable class provides a mapping between contact groups and members |
|
274 of those groups. The groups table provides two fields: contact_group_id and |
|
275 contact_group_member_id. Both of these are foreign keys referencing the contact_id |
|
276 field of the contact table. Using these two fields, lookup can be performed in |
|
277 either direction: members of a group; groups of which item is a member. |
|
278 */ |
|
279 NONSHARABLE_CLASS(CPplGroupsTable) : public CPplTableBase |
|
280 { |
|
281 public: |
|
282 static CPplGroupsTable* NewL(RSqlDatabase& aDatabase); |
|
283 static CPplGroupsTable* NewLC(RSqlDatabase& aDatabase); |
|
284 void CreateInDbL(CContactItem& aItem); |
|
285 void ReadL(CContactItem& aItem); |
|
286 void UpdateL(const CContactItem& aItem); |
|
287 void DeleteL(const CContactItem& aItem, TBool& aLowDiskErrorOccurred); |
|
288 void CreateTableL(); |
|
289 ~CPplGroupsTable(); |
|
290 |
|
291 private: |
|
292 void ConstructL(); |
|
293 CContactIdArray* GetListForItemL(TContactItemId aItemId, TBool aIsGroup); |
|
294 void DeleteItemL(TContactItemId aItemId, TBool& aLowDiskErrorOccurred); |
|
295 CPplGroupsTable(RSqlDatabase& aDatabase); |
|
296 void WriteGroupMembersL(const CContactItem& aGroup); |
|
297 |
|
298 private: |
|
299 CCntSqlStatement* iInsertStmnt; |
|
300 CCntSqlStatement* iSelectGroupsStmnt; |
|
301 CCntSqlStatement* iSelectMembersStmnt; |
|
302 CCntSqlStatement* iDeleteStmnt; |
|
303 CCntSqlStatement* iCountContactsStmnt; |
|
304 |
|
305 RSqlDatabase& iDatabase; |
|
306 }; |
|
307 |
|
308 |
|
309 /** |
|
310 The CPplPredictiveSearchTableBase is a base class for keymap-specific predictive |
|
311 search tables that contain numeric representation of the fields that are checked |
|
312 in predictive search. |
|
313 */ |
|
314 NONSHARABLE_CLASS(CPplPredictiveSearchTableBase) : public CPplTableBase |
|
315 { |
|
316 public: |
|
317 virtual ~CPplPredictiveSearchTableBase(); |
|
318 |
|
319 public: // From CPplTableBase |
|
320 void CreateInDbL(CContactItem& aItem); |
|
321 void UpdateL(const CContactItem& aItem); |
|
322 void DeleteL(const CContactItem& aItem, TBool& aLowDiskErrorOccurred); |
|
323 virtual void CreateTableL() = 0; |
|
324 |
|
325 public: // New pure virtual functions |
|
326 virtual QList<QChar> FillAllTables() const = 0; |
|
327 |
|
328 private: // New pure virtual functions |
|
329 virtual HBufC* TableNameL(const QChar aCh) const = 0; |
|
330 virtual TBool IsValidChar(const QChar aChar) const = 0; |
|
331 |
|
332 virtual void FillKeyboardSpecificFieldsL(RSqlStatement& aSqlStatement, |
|
333 QStringList aTokens) = 0; |
|
334 |
|
335 private: // New virtual functions |
|
336 virtual QStringList |
|
337 GetTableSpecificFields(const CContactItem& aItem, |
|
338 TBool& aMandatoryFieldsPresent) const; |
|
339 |
|
340 public: |
|
341 // Return next table's name, ownership is transferred |
|
342 HBufC* GetNextTableNameL(QList<QChar>& aTables) const; |
|
343 |
|
344 protected: |
|
345 void ConstructL(); |
|
346 CPplPredictiveSearchTableBase(RSqlDatabase& aDatabase, |
|
347 TInt aMaxTokens, |
|
348 TInt aMaxTokenLength); |
|
349 |
|
350 QList<QChar> DetermineTables(QStringList aTokens) const; |
|
351 |
|
352 // aFirstName ownership is not transferred |
|
353 // aLastName ownership is not transferred |
|
354 QStringList GetTokens(QStringList aNonTokenizedFields, |
|
355 HBufC* aFirstName, |
|
356 HBufC* aLastName) const; |
|
357 |
|
358 private: |
|
359 void WriteToDbL(const CContactItem& aItem); |
|
360 |
|
361 // aFirstNameAsNbr OUT: Pointer to first name converted to numbers, |
|
362 // pushed to cleanupstack. Ownership is transferred. |
|
363 // aLastNameAsNbr OUT: Pointer to last name converted to numbers, |
|
364 // pushed to cleanupstack. Ownership is transferred. |
|
365 // aFirstName OUT: Pointer to the first N characters of first name, |
|
366 // pushed to cleanupstack. Ownership is transferred. |
|
367 // aLastName OUT: Pointer to the first N characters of last name, |
|
368 // pushed to cleanupstack. Ownership is transferred. |
|
369 void GetFieldsLC(const CContactItem& aItem, |
|
370 HBufC** aFirstNameAsNbr, |
|
371 HBufC** aLastNameAsNbr, |
|
372 HBufC** aFirstName, |
|
373 HBufC** aLastName) const; |
|
374 |
|
375 // aString ownership is not transferred |
|
376 void AddTokens(HBufC* aString, QStringList& aTokens) const; |
|
377 |
|
378 void GetNextToken(QStringList& aSource, QStringList& aDestination) const; |
|
379 void DeleteFromAllTablesL(TContactItemId aContactId, |
|
380 TBool& aLowDiskErrorOccurred) const; |
|
381 |
|
382 protected: |
|
383 // Owned |
|
384 CCntSqlStatement* iInsertStmnt; |
|
385 // Owned |
|
386 CCntSqlStatement* iDeleteStmnt; |
|
387 // Owned |
|
388 CPcsKeyMap* iKeyMap; |
|
389 |
|
390 RSqlDatabase& iDatabase; |
|
391 |
|
392 // Max amount of tokens that can be stored into predictive search table |
|
393 const TInt iMaxTokens; |
|
394 |
|
395 // Max length of a single token that can be stored into predictive search table |
|
396 const TInt iMaxTokenLength; |
|
397 }; |
|
398 |
|
399 |
|
400 NONSHARABLE_CLASS(CPplPresenceTable) : public CPplTableBase |
|
401 { |
|
402 public: |
|
403 static CPplPresenceTable* NewL(RSqlDatabase& aDatabase); |
|
404 static CPplPresenceTable* NewLC(RSqlDatabase& aDatabase); |
|
405 ~CPplPresenceTable(); |
|
406 private: |
|
407 void ConstructL(); |
|
408 CPplPresenceTable(RSqlDatabase& aDatabase); |
|
409 public: // From CPplTableBase |
|
410 void CreateTableL(); |
|
411 void CreateInDbL(CContactItem& aItem); |
|
412 void UpdateL(const CContactItem& aItem); |
|
413 void DeleteL(const CContactItem& aItem, TBool& aLowDiskErrorOccurred); |
|
414 private: |
|
415 RSqlDatabase& iDatabase; |
|
416 }; |
|
417 /** |
|
418 This class holds a set of contact database preferences and is used in conjunction |
|
419 with the CPplPreferencesPersistor class. |
|
420 */ |
|
421 NONSHARABLE_CLASS(RCntPreferences) |
|
422 { |
|
423 public: |
|
424 TUint DataSchemaVersion() const; |
|
425 TUint DatabaseUid() const; |
|
426 TTime CreationDate() const; |
|
427 TUint PreferCardTemplateId() const; |
|
428 RContactViewSortOrder& PreferredSortOrder(); |
|
429 RCntPreferences(); |
|
430 void Close(); |
|
431 void SetDataSchemaVersion(TUint aDbSchemaVersion); |
|
432 void SetDatabaseUid(TUint aDbUid); |
|
433 void SetCreationDate(TTime aCreationDate); |
|
434 void SetFieldTypeFixsize(TUint aFieldTypeFixsize); |
|
435 void SetPreferredSortOrderL(const RContactViewSortOrder& aSortOrder); |
|
436 void SetPreferCardTemplateId(TUint aTemplateId); |
|
437 |
|
438 private: |
|
439 TUint iDbSchemaVersion; |
|
440 TUint iDbUid; |
|
441 TTime iCreationDate; |
|
442 TUint iPreferCardTemplateId; |
|
443 RContactViewSortOrder iSortOrder; |
|
444 }; |
|
445 |
|
446 /** |
|
447 The CPplPreferencesPersistor class is used to store contact database preferences, |
|
448 as represented by the RCntPreferences class and, currently, stored in the |
|
449 preferences table in the SQLite database. As this information is different from |
|
450 the rest of the data in that it is not contacts data but metadata about the |
|
451 database itself, it is treated differently. Hence, this class is called a persistor |
|
452 and not a table class to emphasise the differences and to provide the flexibility |
|
453 to change the way the preferences information is persist in the future if this is |
|
454 desired. |
|
455 |
|
456 */ |
|
457 NONSHARABLE_CLASS(CPplPreferencesPersistor) : public CBase |
|
458 { |
|
459 public: |
|
460 static CPplPreferencesPersistor* NewL(RSqlDatabase& aDatabase); |
|
461 ~CPplPreferencesPersistor(); |
|
462 |
|
463 void CreateTableL(); |
|
464 |
|
465 // getters |
|
466 TDesC DataSchemaVersion() const; |
|
467 TInt64 MachineIdL(); |
|
468 TTime CreationDateL(); |
|
469 TContactItemId PreferredCardTemplateIdL(); |
|
470 const CArrayFix<CContactDatabase::TSortPref>& PreferredSortOrderL(); |
|
471 TPtrC DatabaseUidL(); |
|
472 |
|
473 // setters |
|
474 void SetMachineIdL(TInt64 aMachineId); |
|
475 void SetPreferredCardTemplateIdL(TContactItemId aTemplateId); |
|
476 void SetPreferredSortOrderL(CArrayFix<CContactDatabase::TSortPref>* aSortOrder); |
|
477 |
|
478 // utility methods |
|
479 void SetGuidL(CContactItem& aContact, TBool& aCompressed); |
|
480 |
|
481 private: |
|
482 CPplPreferencesPersistor(RSqlDatabase& aDatabase); |
|
483 void ConstructL(); |
|
484 void FirstWriteToTableL(); |
|
485 void GenerateMachineUniqueID(); |
|
486 void ReadInStateL(); |
|
487 void PersistStateL(const TDesC& aParam, TInt aValue); |
|
488 |
|
489 private: |
|
490 RSqlDatabase& iDatabase; |
|
491 CCntSqlStatement* iUpdateStmnt; |
|
492 TUint iMachineId; // machine id |
|
493 TTime iCreationDate; // creation data |
|
494 TBuf<40> iUidString; //unique id |
|
495 TContactItemId iPreferCardTemplateId; // template |
|
496 CArrayFix<CContactDatabase::TSortPref>* iSortOrderPrefs; // sort order |
|
497 }; |
|
498 |
|
499 #endif |