Project

General

Profile

1
/**
2
 *  '$RCSfile$'
3
 *  Copyright: 2000-2011 Regents of the University of California and the
4
 *              National Center for Ecological Analysis and Synthesis
5
 *
6
 *   '$Author:  $'
7
 *     '$Date:  $'
8
 *
9
 * This program is free software; you can redistribute it and/or modify
10
 * it under the terms of the GNU General Public License as published by
11
 * the Free Software Foundation; either version 2 of the License, or
12
 * (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 * GNU General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU General Public License
20
 * along with this program; if not, write to the Free Software
21
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
22
 */
23

    
24
package edu.ucsb.nceas.metacat.dataone;
25

    
26
import java.io.IOException;
27
import java.io.InputStream;
28
import java.security.NoSuchAlgorithmException;
29
import java.sql.SQLException;
30
import java.util.Date;
31
import java.util.List;
32

    
33
import org.apache.commons.io.IOUtils;
34
import org.apache.log4j.Logger;
35
import org.dataone.service.exceptions.IdentifierNotUnique;
36
import org.dataone.service.exceptions.InsufficientResources;
37
import org.dataone.service.exceptions.InvalidRequest;
38
import org.dataone.service.exceptions.InvalidSystemMetadata;
39
import org.dataone.service.exceptions.InvalidToken;
40
import org.dataone.service.exceptions.NotAuthorized;
41
import org.dataone.service.exceptions.NotFound;
42
import org.dataone.service.exceptions.NotImplemented;
43
import org.dataone.service.exceptions.ServiceFailure;
44
import org.dataone.service.exceptions.SynchronizationFailed;
45
import org.dataone.service.exceptions.UnsupportedType;
46
import org.dataone.service.mn.tier1.MNCore;
47
import org.dataone.service.mn.tier1.MNRead;
48
import org.dataone.service.mn.tier2.MNAuthorization;
49
import org.dataone.service.mn.tier3.MNStorage;
50
import org.dataone.service.mn.tier4.MNReplication;
51
import org.dataone.service.types.AccessPolicy;
52
import org.dataone.service.types.Checksum;
53
import org.dataone.service.types.ChecksumAlgorithm;
54
import org.dataone.service.types.DescribeResponse;
55
import org.dataone.service.types.Event;
56
import org.dataone.service.types.Group;
57
import org.dataone.service.types.Identifier;
58
import org.dataone.service.types.Log;
59
import org.dataone.service.types.MonitorList;
60
import org.dataone.service.types.Node;
61
import org.dataone.service.types.NodeReference;
62
import org.dataone.service.types.ObjectFormat;
63
import org.dataone.service.types.ObjectList;
64
import org.dataone.service.types.Permission;
65
import org.dataone.service.types.Session;
66
import org.dataone.service.types.Subject;
67
import org.dataone.service.types.SystemMetadata;
68
import org.dataone.service.types.util.ServiceTypeUtil;
69

    
70
import edu.ucsb.nceas.metacat.DocumentImpl;
71
import edu.ucsb.nceas.metacat.EventLog;
72
import edu.ucsb.nceas.metacat.IdentifierManager;
73
import edu.ucsb.nceas.metacat.McdbDocNotFoundException;
74
import edu.ucsb.nceas.metacat.client.InsufficientKarmaException;
75
import edu.ucsb.nceas.metacat.properties.PropertyService;
76
import edu.ucsb.nceas.utilities.PropertyNotFoundException;
77

    
78
/**
79
 * Represents Metacat's implementation of the DataONE Member Node 
80
 * service API. Methods implement the various MN* interfaces, and methods common
81
 * to both Member Node and Coordinating Node interfaces are found in the
82
 * D1NodeService base class.
83
 */
84
public class MNodeService extends D1NodeService implements MNAuthorization,
85
  MNCore, MNRead, MNReplication, MNStorage {
86

    
87
  /* the instance of the MNodeService object */
88
  private static MNodeService instance = null;
89
  
90
  /* the logger instance */
91
  private Logger logMetacat = null;
92

    
93
  /**
94
   * Singleton accessor to get an instance of MNodeService.
95
   * 
96
   * @return instance - the instance of MNodeService
97
   */
98
  public static MNodeService getInstance() {
99
    if (instance == null) {
100

    
101
      instance = new MNodeService();
102
      
103
    }
104
    
105
    return instance;
106
  }
107
  
108
  /**
109
   * Constructor, private for singleton access
110
   */
111
  private MNodeService() {
112
    super();
113
    logMetacat = Logger.getLogger(MNodeService.class);
114
        
115
  }
116
    
117
  /**
118
   * Deletes an object from the Member Node, where the object is either a 
119
   * data object or a science metadata object.
120
   * 
121
   * @param session - the Session object containing the credentials for the Subject
122
   * @param pid - The object identifier to be deleted
123
   * 
124
   * @return pid - the identifier of the object used for the deletion
125
   * 
126
   * @throws InvalidToken
127
   * @throws ServiceFailure
128
   * @throws NotAuthorized
129
   * @throws NotFound
130
   * @throws NotImplemented
131
   * @throws InvalidRequest
132
   */
133
  @Override
134
  public Identifier delete(Session session, Identifier pid) 
135
    throws InvalidToken, ServiceFailure, NotAuthorized, NotFound, 
136
    NotImplemented, InvalidRequest {
137

    
138
    String localId = null;
139
    boolean allowed = false;
140
    Subject subject = session.getSubject();
141
    List<Group> groupList = session.getSubjectList().getGroupList();
142
    String[] groups = new String[groupList.size()];
143
    IdentifierManager im = IdentifierManager.getInstance();
144
    
145
    // put the group names into a string array
146
    if( session != null ) {
147
      for ( int i = 0; i > groupList.size(); i++ ) {
148
        groups[i] = groupList.get(i).getGroupName();
149
        
150
      }
151
    }
152

    
153
    // be sure the user is authenticated for delete()
154
    if (subject.getValue() == null || 
155
        subject.getValue().toLowerCase().equals("public") ) {
156
      throw new NotAuthorized("1320", "The provided identity does not have " +
157
        "permission to DELETE objects on the Member Node.");
158
      
159
    }
160
    
161
    // do we have a valid pid?
162
    if ( pid == null || pid.getValue().trim().equals("") ) {
163
      throw new InvalidRequest("1322", "The provided identifier was invalid.");
164

    
165
    }
166

    
167
    // check for the existing identifier
168
    try {
169
      localId = im.getLocalId(pid.getValue());
170
    
171
    } catch (McdbDocNotFoundException e) {
172
      throw new InvalidRequest("1322", "The object with the provided " +
173
        "identifier was not found.");
174

    
175
    }
176
    
177
    // does the subject have DELETE (a D1 CHANGE_PERMISSION level) priveleges on the pid?
178
    allowed = isAuthorized(session, pid, Permission.CHANGE_PERMISSION);
179
    
180
    if ( allowed ) {
181
      try {
182
        // delete the document
183
        DocumentImpl.delete(localId, subject.getValue(), groups, null);
184
        EventLog.getInstance().log(metacatUrl, subject.getValue(), localId, "delete");
185

    
186
      } catch (McdbDocNotFoundException e) {
187
        throw new InvalidRequest("1322", "The provided identifier was invalid.");
188

    
189
      } catch (SQLException e) {
190
        throw new ServiceFailure("1350", "There was a problem deleting the object." +
191
          "The error message was: " + e.getMessage());
192

    
193
      } catch (InsufficientKarmaException e) {
194
        throw new NotAuthorized("1320", "The provided identity does not have " +
195
        "permission to DELETE objects on the Member Node.");
196
 
197
      } catch (Exception e) { // for some reason DocumentImpl throws a general Exception
198
        throw new ServiceFailure("1350", "There was a problem deleting the object." +
199
            "The error message was: " + e.getMessage());
200

    
201
      }
202

    
203
    } else {
204
      throw new NotAuthorized("1320", "The provided identity does not have " +
205
      "permission to DELETE objects on the Member Node.");
206
      
207
    }
208
    
209
    return pid;
210
  }
211

    
212

    
213
  /**
214
   * Updates an existing object by creating a new object identified by 
215
   * newPid on the Member Node which explicitly obsoletes the object 
216
   * identified by pid through appropriate changes to the SystemMetadata 
217
   * of pid and newPid
218
   * 
219
   * @param session - the Session object containing the credentials for the Subject
220
   * @param pid - The identifier of the object to be updated
221
   * @param object - the new object bytes
222
   * @param sysmeta - the new system metadata describing the object
223
   * 
224
   * @return newPid - the identifier of the new object
225
   * 
226
   * @throws InvalidToken
227
   * @throws ServiceFailure
228
   * @throws NotAuthorized
229
   * @throws NotFound
230
   * @throws NotImplemented
231
   * @throws IdentifierNotUnique
232
   * @throws UnsupportedType
233
   * @throws InsufficientResources
234
   * @throws InvalidSystemMetadata
235
   * @throws InvalidRequest
236
   */
237
  @Override
238
  public Identifier update(Session session, Identifier pid, InputStream object,
239
    Identifier newPid, SystemMetadata sysmeta) 
240
    throws InvalidToken, ServiceFailure, NotAuthorized, IdentifierNotUnique, 
241
    UnsupportedType, InsufficientResources, NotFound, InvalidSystemMetadata, 
242
    NotImplemented, InvalidRequest {
243

    
244
    String localId = null;
245
    boolean allowed = false;
246
    boolean isScienceMetadata = false;
247
    Subject subject = session.getSubject();
248
    List<Group> groupList = session.getSubjectList().getGroupList();
249
    String[] groups = new String[groupList.size()];
250
    IdentifierManager im = IdentifierManager.getInstance();
251

    
252
    // put the group names into a string array
253
    if( session != null ) {
254
      for ( int i = 0; i > groupList.size(); i++ ) {
255
        groups[i] = groupList.get(i).getGroupName();
256
        
257
      }
258
    }
259

    
260
    // be sure the user is authenticated for update()
261
    if (subject.getValue() == null || 
262
        subject.getValue().toLowerCase().equals("public") ) {
263
      throw new NotAuthorized("1200", "The provided identity does not have " +
264
        "permission to UPDATE objects on the Member Node.");
265
      
266
    }
267

    
268
    // do we have a valid pid?
269
    if ( pid == null || pid.getValue().trim().equals("") ) {
270
      throw new InvalidRequest("1202", "The provided identifier was invalid.");
271

    
272
    }
273

    
274
    // check for the existing identifier
275
    try {
276
      localId = im.getLocalId(pid.getValue());
277

    
278
    } catch (McdbDocNotFoundException e) {
279
      throw new InvalidRequest("1202", "The object with the provided " +
280
        "identifier was not found.");
281

    
282
    }
283

    
284
    // does the subject have WRITE ( == update) priveleges on the pid?
285
    allowed = isAuthorized(session, pid, Permission.WRITE);
286

    
287
    if ( allowed ) {
288
      
289
      // get the existing system metadata for the object
290
      SystemMetadata existingSysMeta = getSystemMetadata(session, pid);
291
      
292
      // add the obsoleted pid to the obsoletedBy list
293
      List<Identifier> obsoletedList = existingSysMeta.getObsoletedByList();
294
      obsoletedList.add(pid);
295
      existingSysMeta.setObsoletedByList(obsoletedList);
296
      
297
      // then update the existing system metadata
298
      updateSystemMetadata(existingSysMeta);
299
      
300
      // prep the new system metadata, add pid to the obsoletes list
301
      sysmeta.addObsolete(pid);
302
      
303
      // and insert the new system metadata
304
      insertSystemMetadata(sysmeta);
305
      
306
      isScienceMetadata = isScienceMetadata(sysmeta);
307
      
308
      // do we have XML metadata or a data object?
309
      if ( isScienceMetadata ) {
310
        
311
        // update the science metadata XML document
312
        // TODO: handle non-XML metadata/data documents (like netCDF)
313
        // TODO: don't put objects into memory using stream to string
314
        String objectAsXML = "";
315
        try {
316
          objectAsXML = IOUtils.toString(object, "UTF-8");
317
          localId = insertOrUpdateDocument(objectAsXML, newPid, session, "update");
318
          // register the newPid and the generated localId
319
          if ( newPid != null ) {
320
            im.createMapping(newPid.getValue(), localId);
321
            
322
          }
323
          
324
        } catch (IOException e) {
325
          String msg = "The Node is unable to create the object. " +
326
          "There was a problem converting the object to XML";
327
          logMetacat.info(msg);
328
          throw new ServiceFailure("1310", msg + ": " + e.getMessage());
329
        
330
        }
331
        
332
      } else {
333
        
334
        // update the data object
335
        localId = insertDataObject(object, newPid, session);
336
        // register the newPid and the generated localId
337
        if ( newPid != null ) {
338
          im.createMapping(newPid.getValue(), localId);
339
          
340
        }
341
       
342
      }
343
      // log the update event
344
      EventLog.getInstance().log(metacatUrl, subject.getValue(), localId, "update");
345

    
346
    } else {
347
      throw new NotAuthorized("1200", "The provided identity does not have " +
348
      "permission to UPDATE the object identified by " +
349
      pid.getValue() + " on the Member Node.");
350
      
351
    }
352
    
353
    return pid;
354
  }
355

    
356
  /**
357
   * Called by a Coordinating Node to request that the Member Node create a 
358
   * copy of the specified object by retrieving it from another Member 
359
   * Node and storing it locally so that it can be made accessible to 
360
   * the DataONE system.
361
   * 
362
   * @param session - the Session object containing the credentials for the Subject
363
   * @param sysmeta - Copy of the CN held system metadata for the object
364
   * @param sourceNode - A reference to node from which the content should be 
365
   *                     retrieved. The reference should be resolved by 
366
   *                     checking the CN node registry.
367
   * 
368
   * @return true if the replication succeeds
369
   * 
370
   * @throws ServiceFailure
371
   * @throws NotAuthorized
372
   * @throws NotImplemented
373
   * @throws UnsupportedType
374
   * @throws InsufficientResources
375
   * @throws InvalidRequest
376
   */
377
  @Override
378
  public boolean replicate(Session session, SystemMetadata sysmeta, 
379
    NodeReference sourceNode)
380
    throws NotImplemented, ServiceFailure, NotAuthorized, InvalidRequest,
381
    InsufficientResources, UnsupportedType {
382

    
383
    return false;
384
  }
385

    
386
  /**
387
   * This method provides a lighter weight mechanism than 
388
   * MN_read.getSystemMetadata() for a client to determine basic 
389
   * properties of the referenced object.
390
   * 
391
   * @param session - the Session object containing the credentials for the Subject
392
   * @param pid - the identifier of the object to be described
393
   * 
394
   * @return describeResponse - A set of values providing a basic description 
395
   *                            of the object.
396
   * 
397
   * @throws InvalidToken
398
   * @throws ServiceFailure
399
   * @throws NotAuthorized
400
   * @throws NotFound
401
   * @throws NotImplemented
402
   * @throws InvalidRequest
403
   */
404
  @Override
405
  public DescribeResponse describe(Session session, Identifier pid)
406
    throws InvalidToken, ServiceFailure, NotAuthorized, NotFound,
407
    NotImplemented, InvalidRequest {
408
    
409
    if(session == null) {
410
      throw new InvalidToken("1370", "The session object is null");
411
      
412
    }
413
    
414
    if(pid == null || pid.getValue().trim().equals(""))
415
    {
416
      throw new InvalidRequest("1362", "The object identifier is null. " +
417
        "A valid identifier is required.");
418
        
419
    }
420
    
421
    SystemMetadata sysmeta = getSystemMetadata(session, pid);
422
    DescribeResponse describeResponse = 
423
      new DescribeResponse(sysmeta.getObjectFormat(), 
424
      sysmeta.getSize(), sysmeta.getDateSysMetadataModified(), sysmeta.getChecksum());
425
    
426
    return describeResponse;
427

    
428
  }
429

    
430
  /**
431
   * Return the object identified by the given object identifier
432
   * 
433
   * @param session - the Session object containing the credentials for the Subject
434
   * @param pid - the object identifier for the given object
435
   * 
436
   * @return inputStream - the input stream of the given object
437
   * 
438
   * @throws InvalidToken
439
   * @throws ServiceFailure
440
   * @throws NotAuthorized
441
   * @throws InvalidRequest
442
   * @throws NotImplemented
443
   */
444
  @Override
445
  public InputStream get(Session session, Identifier pid) 
446
    throws InvalidToken, ServiceFailure, NotAuthorized, NotFound, 
447
    NotImplemented, InvalidRequest {
448
    
449
    return super.get(session, pid);
450
    
451
  }
452

    
453
  /**
454
   * Returns a Checksum for the specified object using an accepted hashing algorithm
455
   * 
456
   * @param session - the Session object containing the credentials for the Subject
457
   * @param pid - the object identifier for the given object
458
   * @param algorithm -  the name of an algorithm that will be used to compute 
459
   *                     a checksum of the bytes of the object
460
   * 
461
   * @return checksum - the checksum of the given object
462
   * 
463
   * @throws InvalidToken
464
   * @throws ServiceFailure
465
   * @throws NotAuthorized
466
   * @throws NotFound
467
   * @throws InvalidRequest
468
   * @throws NotImplemented
469
   */
470
  @Override
471
  public Checksum getChecksum(Session session, Identifier pid, String algorithm)
472
    throws InvalidToken, ServiceFailure, NotAuthorized, NotFound,
473
    InvalidRequest, NotImplemented {
474

    
475
    Checksum checksum = null;
476
    
477
    InputStream inputStream = get(session, pid);
478
    
479
    try {
480
      checksum = 
481
        ServiceTypeUtil.checksum(inputStream, ChecksumAlgorithm.convert(algorithm));
482
    
483
    } catch (NoSuchAlgorithmException e) {
484
      throw new ServiceFailure("1410", "The checksum for the object specified by " + 
485
        pid.getValue() +
486
        "could not be returned due to an internal error: " +
487
        e.getMessage());
488
      
489
    } catch (IOException e) {
490
      throw new ServiceFailure("1410", "The checksum for the object specified by " + 
491
        pid.getValue() +
492
        "could not be returned due to an internal error: " +
493
        e.getMessage());
494
      
495
    }
496
    
497
    if ( checksum == null ) {
498
      throw new ServiceFailure("1410", "The checksum for the object specified by " + 
499
        pid.getValue() +
500
        "could not be returned.");
501
      
502
    }
503
    
504
    return checksum;
505
  }
506

    
507
  /**
508
   * Return the system metadata for a given object
509
   * 
510
   * @param session - the Session object containing the credentials for the Subject
511
   * @param pid - the object identifier for the given object
512
   * 
513
   * @return inputStream - the input stream of the given system metadata object
514
   * 
515
   * @throws InvalidToken
516
   * @throws ServiceFailure
517
   * @throws NotAuthorized
518
   * @throws NotFound
519
   * @throws InvalidRequest
520
   * @throws NotImplemented
521
   */
522
  @Override
523
  public SystemMetadata getSystemMetadata(Session session, Identifier pid)
524
      throws InvalidToken, ServiceFailure, NotAuthorized, NotFound,
525
      InvalidRequest, NotImplemented {
526

    
527
    return super.getSystemMetadata(session, pid);
528
  }
529

    
530
  /**
531
   * Retrieve the list of objects present on the MN that match the calling parameters
532
   * 
533
   * @param session - the Session object containing the credentials for the Subject
534
   * @param startTime - Specifies the beginning of the time range from which 
535
   *                    to return object (>=)
536
   * @param endTime - Specifies the beginning of the time range from which 
537
   *                  to return object (>=)
538
   * @param objectFormat - Restrict results to the specified object format
539
   * @param replicaStatus - Indicates if replicated objects should be returned in the list
540
   * @param start - The zero-based index of the first value, relative to the 
541
   *                first record of the resultset that matches the parameters.
542
   * @param count - The maximum number of entries that should be returned in 
543
   *                the response. The Member Node may return less entries 
544
   *                than specified in this value.
545
   * 
546
   * @return objectList - the list of objects matching the criteria
547
   * 
548
   * @throws InvalidToken
549
   * @throws ServiceFailure
550
   * @throws NotAuthorized
551
   * @throws InvalidRequest
552
   * @throws NotImplemented
553
   */
554
  @Override
555
  public ObjectList listObjects(Session session, Date startTime, Date endTime,
556
    ObjectFormat objectFormat, Boolean replicaStatus, Integer start, Integer count)
557
    throws NotAuthorized, InvalidRequest, NotImplemented, ServiceFailure,
558
    InvalidToken {
559

    
560
    ObjectList objectList = null;
561
    
562
    objectList = IdentifierManager.getInstance().querySystemMetadata(startTime, endTime,
563
        objectFormat, replicaStatus, start, count);
564
    
565
    if ( objectList == null ) {
566
      throw new ServiceFailure("1580", "The object list was null.");
567
    }
568
    
569
    return objectList;
570
  }
571

    
572
  /**
573
   * Retrieve the list of objects present on the MN that match the calling parameters
574
   * 
575
   * @return node - the technical capabilities of the Member Node
576
   * 
577
   * @throws ServiceFailure
578
   * @throws NotAuthorized
579
   * @throws InvalidRequest
580
   * @throws NotImplemented
581
   */
582
  @Override
583
  public Node getCapabilities() throws NotImplemented, NotAuthorized,
584
      ServiceFailure, InvalidRequest {
585

    
586
    return null;
587
  }
588

    
589
  /**
590
   * Returns the number of operations that have been serviced by the node 
591
   * over time periods of one and 24 hours.
592
   * 
593
   * @param session - the Session object containing the credentials for the Subject
594
   * @param period - An ISO8601 compatible DateTime range specifying the time 
595
   *                 range for which to return operation statistics.
596
   * @param requestor - Limit to operations performed by given requestor identity.
597
   * @param event -  Enumerated value indicating the type of event being examined
598
   * @param format - Limit to events involving objects of the specified format
599
   * 
600
   * @return the desired log records
601
   * 
602
   * @throws InvalidToken
603
   * @throws ServiceFailure
604
   * @throws NotAuthorized
605
   * @throws InvalidRequest
606
   * @throws NotImplemented
607
   */
608
  @Override
609
  public MonitorList getOperationStatistics(Session session, Integer period,
610
    Subject requestor, Event event, ObjectFormat format) 
611
    throws NotImplemented, ServiceFailure, NotAuthorized, InvalidRequest, 
612
    InsufficientResources, UnsupportedType {
613

    
614
    return null;
615
  }
616

    
617
  /**
618
   * Low level “are you alive” operation. A valid ping response is 
619
   * indicated by a HTTP status of 200.
620
   * 
621
   * @return true if the service is alive
622
   * 
623
   * @throws InvalidToken
624
   * @throws ServiceFailure
625
   * @throws NotAuthorized
626
   * @throws InvalidRequest
627
   * @throws NotImplemented
628
   */
629
  @Override
630
  public boolean ping() 
631
    throws NotImplemented, ServiceFailure, NotAuthorized, InvalidRequest, 
632
    InsufficientResources, UnsupportedType {
633

    
634
    return true;
635
  }
636

    
637
  /**
638
   * A callback method used by a CN to indicate to a MN that it cannot 
639
   * complete synchronization of the science metadata identified by pid.  Log
640
   * the event in the metacat event log.
641
   * 
642
   * @param session
643
   * @param syncFailed
644
   * 
645
   * @throws ServiceFailure
646
   * @throws NotAuthorized
647
   * @throws InvalidRequest
648
   * @throws NotImplemented
649
   */
650
  @Override
651
  public void synchronizationFailed(Session session, SynchronizationFailed syncFailed)
652
      throws NotImplemented, ServiceFailure, NotAuthorized, InvalidRequest {
653

    
654
    String localId;
655
    
656
    try {
657
      localId = IdentifierManager.getInstance().getLocalId(syncFailed.getPid().getValue());
658
    } catch (McdbDocNotFoundException e) {
659
      throw new ServiceFailure("2161", "The identifier specified by " +
660
          syncFailed.getPid().getValue() + 
661
          " was not found on this node.");
662
      
663
    }
664
    // TODO: update the CN URL below when the CNRead.SynchronizationFailed
665
    // method is changed to include the URL as a parameter
666
    logMetacat.debug("Synchronization for the object identified by " +
667
      syncFailed.getPid().getValue() + 
668
      " failed from " +
669
      "CN URL WILL GO HERE." +
670
      " Logging the event to the Metacat EventLog as a 'syncFailed' event.");
671
    EventLog.getInstance().log("CN URL WILL GO HERE", 
672
        session.getSubject().getValue(), localId, "syncFailed");
673

    
674
  }
675

    
676
}
(5-5/8)