Make unit tests build again
[qtcontacts-tracker:qtcontacts-tracker.git] / qtrackercontactsaverequest.cpp
1 /****************************************************************************
2 **
3 ** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies).
4 ** All rights reserved.
5 ** Contact: Nokia Corporation (qt-info@nokia.com)
6 **
7 ** This file is part of the Qt Mobility Components.
8 **
9 ** $QT_BEGIN_LICENSE:LGPL$
10 ** No Commercial Usage
11 ** This file contains pre-release code and may not be distributed.
12 ** You may use this file in accordance with the terms and conditions
13 ** contained in the Technology Preview License Agreement accompanying
14 ** this package.
15 **
16 ** GNU Lesser General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU Lesser
18 ** General Public License version 2.1 as published by the Free Software
19 ** Foundation and appearing in the file LICENSE.LGPL included in the
20 ** packaging of this file.  Please review the following information to
21 ** ensure the GNU Lesser General Public License version 2.1 requirements
22 ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
23 **
24 ** In addition, as a special exception, Nokia gives you certain additional
25 ** rights.  These rights are described in the Nokia Qt LGPL Exception
26 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
27 **
28 ** If you have questions regarding the use of this file, please contact
29 ** Nokia at qt-info@nokia.com.
30 **
31 **
32 **
33 **
34 **
35 **
36 **
37 **
38 ** $QT_END_LICENSE$
39 **
40 ****************************************************************************/
41
42 #include "qtrackercontactsaverequest.h"
43 #include "trackerchangelistener.h"
44
45 #include <QtTracker/Tracker>
46 using namespace SopranoLive;
47
48 #include "qtrackercontactslive.h"
49
50 // TODO better error handling when saving
51 QTrackerContactSaveRequest::QTrackerContactSaveRequest(QContactAbstractRequest* req, QContactManagerEngine* parent)
52 : QObject(parent), QTrackerContactAsyncRequest(req), errorCount(0)
53 {
54     Q_ASSERT(req);
55     Q_ASSERT(req->type() == QContactAbstractRequest::ContactSaveRequest);
56     Q_ASSERT(parent);
57
58     QContactSaveRequest* r = qobject_cast<QContactSaveRequest*>(req);
59     if (!r) {
60         QContactManagerEngine::updateRequestState(req, QContactAbstractRequest::FinishedState);
61         return;
62     }
63
64     QList<QContact> contacts = r->contacts();
65
66     if(contacts.isEmpty()) {
67         QMap<int, QContactManager::Error> errors; 
68         errors[0] = QContactManager::BadArgumentError;
69         QContactSaveRequest* saveRequest = qobject_cast<QContactSaveRequest*>(req);
70         QContactManagerEngine::updateContactSaveRequest(saveRequest, contacts, QContactManager::BadArgumentError,
71                                              errors);
72         return;
73     }
74
75     QContactManagerEngine::updateRequestState(req, QContactAbstractRequest::ActiveState);
76
77     TrackerChangeListener *changeListener = new TrackerChangeListener(parent, this);
78     connect(changeListener, SIGNAL(contactsChanged(const QList<QContactLocalId> &)),SLOT(onTrackerSignal(const QList<QContactLocalId> &)));
79     connect(changeListener, SIGNAL(contactsAdded(const QList<QContactLocalId> &)),SLOT(onTrackerSignal(const QList<QContactLocalId> &)));
80
81     // Save contacts with batch size
82     /// @todo where to get reasonable batch size
83     int batchSize = 100;
84     for (int i = 0; i < contacts.size(); i+=batchSize) {
85         saveContacts(contacts.mid(i, batchSize));
86     }
87 }
88
89 void QTrackerContactSaveRequest::onTrackerSignal(const QList<QContactLocalId> &ids)
90 {
91     computeProgress(ids);
92 }
93
94 void QTrackerContactSaveRequest::computeProgress(const QList<QContactLocalId> &addedIds)
95 {
96     Q_ASSERT(req->type() == QContactAbstractRequest::ContactSaveRequest);
97     QContactSaveRequest* r = qobject_cast<QContactSaveRequest*>(req);
98     if (!r) {
99         QContactManagerEngine::updateRequestState(req, QContactAbstractRequest::FinishedState);
100         return;
101     }
102
103     foreach (QContactLocalId id, addedIds) {
104         pendingContactIds.remove(id);
105         // since if was OK, remove entry for error
106         errorsOfContactsFinished.remove(id2Index[id]);
107     }
108
109     if (pendingContactIds.count() == 0) {
110         // compute master error - part of qtcontacts api
111         QContactManager::Error error = QContactManager::NoError;
112         
113         foreach(QContactManager::Error err, errorsOfContactsFinished.values()) {
114             if( QContactManager::NoError != err )
115             {
116                 error = err;
117                 break;
118             }
119         }
120
121         QContactManagerEngine::updateContactSaveRequest(r, contactsFinished, error, errorsOfContactsFinished);
122         QContactManagerEngine::updateRequestState(req, QContactAbstractRequest::FinishedState);
123     }
124 }
125
126 void QTrackerContactSaveRequest::saveContacts(const QList<QContact> &contacts)
127 {
128     QContactManagerEngine *engine = qobject_cast<QContactManagerEngine *> (parent());
129     Q_ASSERT(engine);
130
131     QSettings definitions(QSettings::IniFormat, QSettings::UserScope, "Nokia", "Trackerplugin");
132     QTrackerContactsLive cLive;
133     RDFServicePtr service = cLive.service();
134
135     foreach(QContact contact, contacts) {
136 /*
137         Validation is disabled because it blocks saving contacts parsed from vcards
138         TODO left the commented code while opaque (custom) details are under discussion as remainder
139         QContactManager::Error error;
140         if(!engine->validateContact(contact, error)) {
141             contactsFinished << contact;
142             errorsOfContactsFinished[errorCount++] =  error;
143             computeProgress(QList<QContactLocalId>());
144             continue;
145         }
146 */
147         Live<nco::PersonContact> ncoContact;
148         bool newContact = false;
149
150         if(contact.localId() == 0) {
151             // Save new contact. compute ID
152             bool ok;
153             // what if both processes read in the same time and write at the same time, no increment
154             unsigned int m_lastUsedId = definitions.value("nextAvailableContactId", "1").toUInt(&ok);
155             definitions.setValue("nextAvailableContactId", QString::number(++m_lastUsedId));
156
157             ncoContact = service->liveNode(QUrl("contact:"+(QString::number(m_lastUsedId))));
158             QContactId id;
159             id.setLocalId(m_lastUsedId);
160             id.setManagerUri(engine->managerUri());
161             contact.setId(id);
162             ncoContact->setContactUID(QString::number(m_lastUsedId));
163             ncoContact->setContentCreated(QDateTime::currentDateTime());
164             newContact = true;
165         }  else {
166             ncoContact = service->liveNode(QUrl("contact:"+QString::number(contact.localId())));
167             /// @note Following needed in case we save new contact with given localId
168             ncoContact->setContactUID(QString::number(contact.localId()));
169             ncoContact->setContentLastModified(QDateTime::currentDateTime());
170         }
171         pendingContactIds.insert(contact.localId());
172
173         // if there are work related details, need to be saved to Affiliation.
174         if( QTrackerContactSaveRequest::contactHasWorkRelatedDetails(contact)) {
175             addAffiliation(service, contact.localId());
176         }
177
178         // Add a special tag for contact added from addressbook, not from fb, telepathy etc.
179         // this is important when returning contacts to sync team
180         RDFVariable rdfContact = RDFVariable::fromType<nco::PersonContact>();
181         rdfContact.property<nco::contactUID>() = LiteralValue(QString::number(contact.localId()));
182         addTag(service, rdfContact, "addressbook");
183
184         saveContactDetails( service, ncoContact, contact, newContact);
185
186         // name & nickname - different way from other details
187         cLive.setLiveContact(ncoContact);
188         cLive.setQContact(contact);
189         if( !contact.detail<QContactName>().isEmpty() || !contact.detail<QContactNickname>().isEmpty() ) {
190             cLive.saveName();
191         }
192
193         contactsFinished << contact;
194         id2Index[contact.localId()] = errorCount;
195         // we fill error here - once response come that everything is OK, remove entry for this contact
196         errorsOfContactsFinished[errorCount++] =  QContactManager::BadArgumentError;
197     }
198     // remember to commit the transaction, otherwise all changes will be rolled back.
199     cLive.commit();
200 }
201
202
203 QTrackerContactSaveRequest::~QTrackerContactSaveRequest()
204 {
205     // TODO Auto-generated destructor stub
206 }
207
208 /*!
209 * Saving has to go in such way that all names are saved at once, all phone numbers together
210 * filled to rdfupdate query etc.
211 * This method goes through the contact and collect which contact detail definitions are there
212 */
213 QStringList QTrackerContactSaveRequest::detailsDefinitionsInContact(const QContact &c)
214 {
215     QStringList definitions;
216     foreach(const QContactDetail& det, c.details())
217         {
218             definitions << det.definitionName();
219         }
220     definitions.removeDuplicates();
221     return definitions;
222 }
223
224 //! Just moving this code out of saveContact to make it shorter
225 bool QTrackerContactSaveRequest::contactHasWorkRelatedDetails(const QContact &c)
226 {
227     foreach(const QContactDetail& det, c.details())
228     {
229         if( det.contexts().contains(QContactDetail::ContextWork))
230            return true;
231     }
232     return false;
233 }
234
235 // create nco::Affiliation if there is not one already in tracker
236 void QTrackerContactSaveRequest::addAffiliation(RDFServicePtr service, QContactLocalId contactId)
237 {
238     Live<nco::PersonContact> ncoContact = service->liveNode(QUrl("contact:"+(QString::number(contactId))));
239     Live<nco::Affiliation> ncoAffiliation = service->liveNode(QUrl("affiliation:"+(QString::number(contactId))));
240     ncoContact->setHasAffiliation(ncoAffiliation);
241 }
242
243 void QTrackerContactSaveRequest::saveContactDetails( RDFServicePtr service,
244                                                 Live<nco::PersonContact>& ncoContact,
245                                                 const QContact& contact,
246                                                 bool newContact)
247 {
248     QStringList detailDefinitionsToSave = detailsDefinitionsInContact(contact);
249
250     // all the rest might need to save to PersonContact and to Affiliation contact
251     RDFVariable rdfPerson = RDFVariable::fromType<nco::PersonContact>();
252     rdfPerson.property<nco::contactUID>() = LiteralValue(QString::number(contact.localId()));
253
254     if(not newContact) {
255         // Delete all existing phone numbers - office and home
256         deletePhoneNumbers(service, rdfPerson);
257     }
258
259     foreach(QString definition, detailDefinitionsToSave)
260     {
261         QList<QContactDetail> details = contact.details(definition);
262         Q_ASSERT(!details.isEmpty());
263
264         RDFVariable rdfAffiliation;
265         RDFVariable rdfPerson1;
266         rdfPerson1.property<nco::hasAffiliation>() = rdfAffiliation;
267         rdfPerson1.property<nco::contactUID>() = LiteralValue(QString::number(contact.localId()));
268
269         QList<QContactDetail> workDetails;
270         QList<QContactDetail> homeDetails;
271         foreach(const QContactDetail& det, details) {
272             // details can be for both contexts, so check for both seperately
273             if( det.contexts().contains(QContactDetail::ContextWork) ) {
274                 workDetails << det;
275             }
276             if( det.contexts().contains(QContactDetail::ContextHome)) {
277                 homeDetails << det;
278             }
279             if( !det.contexts().contains(QContactDetail::ContextHome)
280                 && !det.contexts().contains(QContactDetail::ContextWork)) {
281                 homeDetails << det;
282             }
283         }
284
285         /* Save details */
286         if(definition == QContactPhoneNumber::DefinitionName) {
287             if (!homeDetails.isEmpty()) {
288                 savePhoneNumbers(service, rdfPerson, homeDetails, newContact);
289             }
290             if( !workDetails.isEmpty()) {
291                 savePhoneNumbers(service, rdfAffiliation, workDetails, newContact);
292             }
293         }
294         else if(definition == QContactEmailAddress::DefinitionName) {
295             if (!homeDetails.isEmpty())
296                 saveEmails(service, rdfPerson, homeDetails, newContact);
297             if( !workDetails.isEmpty())
298                 saveEmails(service, rdfAffiliation, workDetails, newContact);
299         }
300         else if(definition == QContactAddress::DefinitionName) {
301             if (!homeDetails.isEmpty())
302                 saveAddresses(service, rdfPerson, homeDetails, newContact);
303             if( !workDetails.isEmpty())
304                 saveAddresses(service, rdfAffiliation, workDetails, newContact);
305         }
306         else if(definition == QContactUrl::DefinitionName) {
307             if (!homeDetails.isEmpty())
308                 saveUrls(service, rdfPerson, homeDetails, newContact);
309             if( !workDetails.isEmpty())
310                 saveUrls(service, rdfAffiliation, workDetails, newContact);
311         }
312         else {
313             // TODO refactor (bug: editing photo doesn't work)
314             foreach(const QContactDetail &det, details )
315             {
316                 definition = det.definitionName();
317                 if(definition == QContactAvatar::DefinitionName) {
318                     QUrl avatar = det.value(QContactAvatar::FieldAvatar);
319                     Live<nie::DataObject> fdo = service->liveNode( avatar );
320                     ncoContact->setPhoto(fdo);
321                 }
322                 if(definition == QContactBirthday::DefinitionName) {
323                     ncoContact->setBirthDate(QDateTime(det.variantValue(QContactBirthday::FieldBirthday).toDate(), QTime(), Qt::UTC));
324                 }
325             } // end foreach detail
326         }
327     }
328 }
329
330 // Remove all existing references to phone numbers from the contact so that edits are
331 // reflected to Tracker correctly.
332 // Delete the references to phone numbers - not the numbers themselves as they remain in tracker
333 // with their canonical URI form - might be linked to history.
334 void QTrackerContactSaveRequest::deletePhoneNumbers(RDFServicePtr service, const RDFVariable& rdfContactIn)
335 {
336     {
337         RDFUpdate up;
338         RDFVariable rdfContact = rdfContactIn.deepCopy();
339         up.addDeletion(rdfContact, nco::hasPhoneNumber::iri(), rdfContact.property<nco::hasPhoneNumber>());
340         service->executeQuery(up);
341     }
342
343     // affiliation
344     {
345         RDFUpdate up;
346         RDFVariable rdfContact = rdfContactIn.deepCopy().property<nco::hasAffiliation>();
347         up.addDeletion(rdfContact, nco::hasPhoneNumber::iri(), rdfContact.property<nco::hasPhoneNumber>());
348         service->executeQuery(up);
349     }
350 }
351
352 /*!
353  * write all phone numbers on one query to tracker
354  * TODO this is temporary code for creating new, saving contacts need to handle only what was
355  * changed.
356  */
357 void QTrackerContactSaveRequest::savePhoneNumbers(RDFServicePtr service, RDFVariable &var, const QList<QContactDetail> &details, bool newContact )
358 {
359     RDFUpdate up;
360     RDFVariable varForInsert = var.deepCopy();
361     foreach(const QContactDetail& det, details)
362     {
363         QString formattedValue = det.value(QContactPhoneNumber::FieldNumber);
364         // Strip RFC 3966 visual-separators reg exp "[(|-|.|)| ]"
365         QString value = formattedValue.replace( QRegExp("[\\(|" \
366                                                         "\\-|" \
367                                                         "\\.|" \
368                                                         "\\)|" \
369                                                         " ]"), "");
370         // Temporary, because affiliation is still used - to be refactored next week to use Live nodes
371         // using RFC 3966 canonical URI form
372         QUrl newPhone = QString("tel:%1").arg(value);
373         Live<nco::PhoneNumber> ncoPhone = service->liveNode(newPhone);
374         if(not newContact) {
375             ncoPhone->remove();
376         }
377
378         QStringList subtypes = det.value<QStringList>(QContactPhoneNumber::FieldSubTypes);
379
380         if( subtypes.contains(QContactPhoneNumber::SubTypeMobile))
381             up.addInsertion(newPhone, rdf::type::iri(), nco::CellPhoneNumber::iri());
382         else if( subtypes.contains(QContactPhoneNumber::SubTypeCar))
383             up.addInsertion(newPhone, rdf::type::iri(), nco::CarPhoneNumber::iri());
384         else if( subtypes.contains(QContactPhoneNumber::SubTypeBulletinBoardSystem))
385             up.addInsertion(newPhone, rdf::type::iri(), nco::BbsNumber::iri());
386         else if( subtypes.contains(QContactPhoneNumber::SubTypeFacsimile))
387             up.addInsertion(newPhone, rdf::type::iri(), nco::FaxNumber::iri());
388         else if( subtypes.contains(QContactPhoneNumber::SubTypeModem))
389             up.addInsertion(newPhone, rdf::type::iri(), nco::ModemNumber::iri());
390         else if( subtypes.contains(QContactPhoneNumber::SubTypePager))
391             up.addInsertion(newPhone, rdf::type::iri(), nco::PagerNumber::iri());
392         else if( subtypes.contains(QContactPhoneNumber::SubTypeMessagingCapable))
393             up.addInsertion(newPhone, rdf::type::iri(), nco::MessagingNumber::iri());
394         else
395             up.addInsertion(newPhone, rdf::type::iri(), nco::VoicePhoneNumber::iri());
396
397         up.addInsertion(newPhone, nco::phoneNumber::iri(), LiteralValue(value));
398         up.addInsertion(varForInsert, nco::hasPhoneNumber::iri(), newPhone);
399     }
400     service->executeQuery(up);
401 }
402
403 /*!
404  * write all phone numbers on one query to tracker
405  * TODO this is temporary code for creating new, saving contacts need to handle only what was
406  * changed.
407  */
408 void QTrackerContactSaveRequest::saveEmails(RDFServicePtr service, RDFVariable &var, const QList<QContactDetail> &details, bool newContact )
409 {
410     RDFUpdate up;
411     RDFVariable varForInsert = var.deepCopy();
412     RDFVariable emails = var.property<nco::hasEmailAddress>();
413     if(not newContact) {
414         // delete previous references - keep email IRIs
415         up.addDeletion(RDFVariableStatement(var, nco::hasEmailAddress::iri(), emails));
416     }
417
418     foreach(const QContactDetail& det, details)
419     {
420         QString value = det.value(QContactEmailAddress::FieldEmailAddress);
421         // Temporary, because affiliation is still used - to be refactored next week to use only Live nodes
422         QUrl newEmail = QString("mailto:%1").arg(value);
423         Live<nco::EmailAddress> ncoEmail = service->liveNode(newEmail);
424         up.addInsertion(newEmail, rdf::type::iri(), nco::EmailAddress::iri());
425         up.addInsertion(newEmail, nco::emailAddress::iri(), LiteralValue(value));
426         up.addInsertion(RDFVariableStatement(varForInsert, nco::hasEmailAddress::iri(), newEmail));
427     }
428     service->executeQuery(up);
429 }
430
431 /*!
432  * write all Urls
433  * TODO this is temporary code for creating new, saving contacts need to handle only what was
434  * changed.
435  */
436 void QTrackerContactSaveRequest::saveUrls(RDFServicePtr service, RDFVariable &rdfContact, const QList<QContactDetail> &details, bool newContact )
437 {
438     RDFUpdate up;
439     RDFVariable varForInsert = rdfContact.deepCopy();
440     RDFVariable urls = rdfContact.property<nco::url>();
441     RDFVariable urltypes = urls.property<rdf::type>();
442
443     RDFVariable websiteUrls = rdfContact.property<nco::websiteUrl>();
444     RDFVariable websiteUrlTypes = websiteUrls.property<rdf::type>();
445
446     if(not newContact) {
447         // first part - deleting previous before adding new again is to be removed
448         up.addDeletion(RDFVariableStatement(rdfContact, nco::url::iri(), urls));
449         up.addDeletion(RDFVariableStatement(rdfContact, nco::websiteUrl::iri(), websiteUrls));
450     }
451
452     // second part, write all urls
453     foreach(const QContactDetail& det, details)
454     {
455         QUrl newUrl(det.value(QContactUrl::FieldUrl));//::tracker()->createLiveNode().uri();
456         if(det.value(QContactUrl::FieldSubType) == QContactUrl::SubTypeFavourite)
457         {
458             up.addInsertion(varForInsert, nco::url::iri(), newUrl);
459         }
460         else // if not favourite, then homepage. don't support other
461         {
462             up.addInsertion(varForInsert, nco::websiteUrl::iri(), newUrl); // add it to contact
463         }
464     }
465     service->executeQuery(up);
466 }
467
468 /*!
469  * write all phone numbers on one query to tracker
470  * TODO this is temporary code for creating new, saving contacts need to handle only what was
471  * changed.
472  */
473 void QTrackerContactSaveRequest::saveAddresses(RDFServicePtr service, RDFVariable &var, const QList<QContactDetail> &details, bool newContact )
474 {
475     RDFUpdate up;
476     RDFVariable varForInsert = var.deepCopy();
477     RDFVariable addresses = var.property<nco::hasPostalAddress>();
478     RDFVariable types = addresses.property<rdf::type>();
479     if(not newContact) {
480         up.addDeletion(RDFVariableStatement(var, nco::hasPostalAddress::iri(), addresses));
481         up.addDeletion(addresses, rdf::type::iri(), types);
482     }
483     foreach(const QContactDetail& det, details)
484     {
485         QUrl newPostalAddress = ::tracker()->createLiveNode().uri();
486         // TODO     nco:DomesticDeliveryAddress, nco:InternationalDeliveryAddress, nco:ParcelDeliveryAddress
487         up.addInsertion(newPostalAddress, rdf::type::iri(), nco::PostalAddress::iri());
488         if( det.hasValue(QContactAddress::FieldStreet))
489             up.addInsertion(newPostalAddress, nco::streetAddress::iri(), LiteralValue(det.value(QContactAddress::FieldStreet)));
490         if( det.hasValue(QContactAddress::FieldLocality))
491             up.addInsertion(newPostalAddress, nco::locality::iri(), LiteralValue(det.value(QContactAddress::FieldLocality)));
492         if( det.hasValue(QContactAddress::FieldCountry))
493             up.addInsertion(newPostalAddress, nco::country::iri(), LiteralValue(det.value(QContactAddress::FieldCountry)));
494         if( det.hasValue(QContactAddress::FieldPostcode))
495             up.addInsertion(newPostalAddress, nco::postalcode::iri(), LiteralValue(det.value(QContactAddress::FieldPostcode)));
496         if( det.hasValue(QContactAddress::FieldRegion))
497             up.addInsertion(newPostalAddress, nco::region::iri(), LiteralValue(det.value(QContactAddress::FieldRegion)));
498
499         up.addInsertion(RDFVariableStatement(varForInsert, nco::hasPostalAddress::iri(), newPostalAddress));
500     }
501     service->executeQuery(up);
502 }
503
504 /*!
505  * Not very good solution, but we add "addressbook" tag to identify which contacts
506  * are added but addressbook ( in order to separate them from facebook and telepathy
507  * contacts
508  */
509 void QTrackerContactSaveRequest::createTagIfItDoesntExistAlready(SopranoLive::RDFServicePtr service, const QString &tag)
510 {
511     static bool checked = false;
512     // only once, if someone remove tag we are in problems (lost contacts)
513     if( !checked )
514     {
515         checked = true;
516         RDFVariable rdfTag = RDFVariable::fromType<nao::Tag>();
517         RDFVariable labelVar = rdfTag.optional().property<nao::prefLabel>();
518         labelVar = LiteralValue(tag);
519         RDFFilter doesntExist = labelVar.isBound().not_();// do not create if it already exist
520
521         RDFUpdate up;
522
523         QUrl newTag = ::tracker()->createLiveNode().uri();
524         rdfTag = newTag;
525         QList<RDFVariableStatement> insertions;
526         insertions << RDFVariableStatement(rdfTag, rdf::type::iri(), nao::Tag::iri())
527         << RDFVariableStatement(newTag, nao::prefLabel::iri(), labelVar);
528         up.addInsertion(insertions); // this way we apply filter doesntExist to both insertions
529         service->executeQuery(up);
530     }
531 }
532
533 void QTrackerContactSaveRequest::addTag(RDFServicePtr service, RDFVariable &var, const QString &tag)
534 {
535     // TODO do all in one RDF query: create tag if not existing
536     createTagIfItDoesntExistAlready(service, tag);
537     RDFUpdate up;
538     RDFVariable rdftag;
539     rdftag.property<nao::prefLabel>() = LiteralValue(tag);
540     up.addInsertion(var, nao::hasTag::iri(), rdftag);
541     service->executeQuery(up);
542 }