Project

General

Profile

1 6179 cjones
/**
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 6228 cjones
import java.io.IOException;
27 6179 cjones
import java.io.InputStream;
28 6228 cjones
import java.security.NoSuchAlgorithmException;
29 6250 cjones
import java.sql.SQLException;
30 6600 cjones
import java.util.ArrayList;
31 6525 leinfelder
import java.util.Calendar;
32 6179 cjones
import java.util.Date;
33 6250 cjones
import java.util.List;
34 6389 leinfelder
import java.util.Timer;
35 6179 cjones
36 6542 leinfelder
import javax.servlet.http.HttpServletRequest;
37
38 6258 cjones
import org.apache.commons.io.IOUtils;
39 6179 cjones
import org.apache.log4j.Logger;
40 6528 cjones
import org.dataone.client.CNode;
41 6332 leinfelder
import org.dataone.client.D1Client;
42
import org.dataone.client.MNode;
43 6552 leinfelder
import org.dataone.client.auth.CertificateManager;
44
import org.dataone.configuration.Settings;
45 6179 cjones
import org.dataone.service.exceptions.IdentifierNotUnique;
46
import org.dataone.service.exceptions.InsufficientResources;
47
import org.dataone.service.exceptions.InvalidRequest;
48
import org.dataone.service.exceptions.InvalidSystemMetadata;
49
import org.dataone.service.exceptions.InvalidToken;
50
import org.dataone.service.exceptions.NotAuthorized;
51
import org.dataone.service.exceptions.NotFound;
52
import org.dataone.service.exceptions.NotImplemented;
53
import org.dataone.service.exceptions.ServiceFailure;
54 6185 leinfelder
import org.dataone.service.exceptions.SynchronizationFailed;
55 6179 cjones
import org.dataone.service.exceptions.UnsupportedType;
56 6622 leinfelder
import org.dataone.service.exceptions.VersionMismatch;
57 6366 leinfelder
import org.dataone.service.mn.tier1.v1.MNCore;
58
import org.dataone.service.mn.tier1.v1.MNRead;
59
import org.dataone.service.mn.tier2.v1.MNAuthorization;
60
import org.dataone.service.mn.tier3.v1.MNStorage;
61
import org.dataone.service.mn.tier4.v1.MNReplication;
62 6573 cjones
import org.dataone.service.types.v1.AccessPolicy;
63 6366 leinfelder
import org.dataone.service.types.v1.Checksum;
64
import org.dataone.service.types.v1.DescribeResponse;
65
import org.dataone.service.types.v1.Event;
66
import org.dataone.service.types.v1.Group;
67
import org.dataone.service.types.v1.Identifier;
68
import org.dataone.service.types.v1.Log;
69
import org.dataone.service.types.v1.LogEntry;
70
import org.dataone.service.types.v1.MonitorInfo;
71
import org.dataone.service.types.v1.MonitorList;
72
import org.dataone.service.types.v1.Node;
73 6600 cjones
import org.dataone.service.types.v1.NodeList;
74 6366 leinfelder
import org.dataone.service.types.v1.NodeReference;
75
import org.dataone.service.types.v1.NodeState;
76
import org.dataone.service.types.v1.NodeType;
77
import org.dataone.service.types.v1.ObjectFormatIdentifier;
78
import org.dataone.service.types.v1.ObjectList;
79
import org.dataone.service.types.v1.Permission;
80
import org.dataone.service.types.v1.Ping;
81 6528 cjones
import org.dataone.service.types.v1.ReplicationStatus;
82 6366 leinfelder
import org.dataone.service.types.v1.Schedule;
83
import org.dataone.service.types.v1.Service;
84
import org.dataone.service.types.v1.Services;
85
import org.dataone.service.types.v1.Session;
86
import org.dataone.service.types.v1.Subject;
87 6600 cjones
import org.dataone.service.types.v1.SubjectList;
88 6366 leinfelder
import org.dataone.service.types.v1.Synchronization;
89
import org.dataone.service.types.v1.SystemMetadata;
90
import org.dataone.service.types.v1.util.ChecksumUtil;
91 6476 jones
import org.dataone.service.util.Constants;
92 6179 cjones
93 6250 cjones
import edu.ucsb.nceas.metacat.DocumentImpl;
94 6234 cjones
import edu.ucsb.nceas.metacat.EventLog;
95 6230 cjones
import edu.ucsb.nceas.metacat.IdentifierManager;
96 6234 cjones
import edu.ucsb.nceas.metacat.McdbDocNotFoundException;
97 6389 leinfelder
import edu.ucsb.nceas.metacat.MetacatHandler;
98 6250 cjones
import edu.ucsb.nceas.metacat.client.InsufficientKarmaException;
99 6260 cjones
import edu.ucsb.nceas.metacat.database.DBConnection;
100
import edu.ucsb.nceas.metacat.database.DBConnectionPool;
101 6340 cjones
import edu.ucsb.nceas.metacat.properties.PropertyService;
102 6542 leinfelder
import edu.ucsb.nceas.metacat.util.SystemUtil;
103 6340 cjones
import edu.ucsb.nceas.utilities.PropertyNotFoundException;
104 6230 cjones
105 6179 cjones
/**
106
 * Represents Metacat's implementation of the DataONE Member Node
107
 * service API. Methods implement the various MN* interfaces, and methods common
108
 * to both Member Node and Coordinating Node interfaces are found in the
109
 * D1NodeService base class.
110 6288 cjones
 *
111
 * Implements:
112
 * MNCore.ping()
113
 * MNCore.getLogRecords()
114
 * MNCore.getObjectStatistics()
115
 * MNCore.getOperationStatistics()
116
 * MNCore.getStatus()
117
 * MNCore.getCapabilities()
118
 * MNRead.get()
119
 * MNRead.getSystemMetadata()
120
 * MNRead.describe()
121
 * MNRead.getChecksum()
122
 * MNRead.listObjects()
123
 * MNRead.synchronizationFailed()
124
 * MNAuthorization.isAuthorized()
125
 * MNAuthorization.setAccessPolicy()
126
 * MNStorage.create()
127
 * MNStorage.update()
128
 * MNStorage.delete()
129
 * MNReplication.replicate()
130
 *
131 6179 cjones
 */
132 6599 cjones
public class MNodeService extends D1NodeService
133
    implements MNAuthorization, MNCore, MNRead, MNReplication, MNStorage {
134 6179 cjones
135 6475 jones
    /* the logger instance */
136
    private Logger logMetacat = null;
137 6241 cjones
138 6475 jones
    /**
139
     * Singleton accessor to get an instance of MNodeService.
140
     *
141
     * @return instance - the instance of MNodeService
142
     */
143 6542 leinfelder
    public static MNodeService getInstance(HttpServletRequest request) {
144
        return new MNodeService(request);
145 6179 cjones
    }
146
147 6475 jones
    /**
148
     * Constructor, private for singleton access
149
     */
150 6542 leinfelder
    private MNodeService(HttpServletRequest request) {
151
        super(request);
152 6475 jones
        logMetacat = Logger.getLogger(MNodeService.class);
153 6552 leinfelder
154
        // set the Member Node certificate file location
155
        CertificateManager.getInstance().setCertificateLocation(Settings.getConfiguration().getString("D1Client.certificate.file"));
156 6310 cjones
    }
157 6475 jones
158
    /**
159
     * Deletes an object from the Member Node, where the object is either a
160
     * data object or a science metadata object.
161
     *
162
     * @param session - the Session object containing the credentials for the Subject
163
     * @param pid - The object identifier to be deleted
164
     *
165
     * @return pid - the identifier of the object used for the deletion
166
     *
167
     * @throws InvalidToken
168
     * @throws ServiceFailure
169
     * @throws NotAuthorized
170
     * @throws NotFound
171
     * @throws NotImplemented
172
     * @throws InvalidRequest
173
     */
174
    @Override
175
    public Identifier delete(Session session, Identifier pid)
176 6610 cjones
        throws InvalidToken, ServiceFailure, NotAuthorized, NotFound, NotImplemented {
177 6475 jones
178
        String localId = null;
179
        boolean allowed = false;
180 6532 leinfelder
        String username = Constants.SUBJECT_PUBLIC;
181 6475 jones
        String[] groupnames = null;
182
        if (session != null) {
183
            username = session.getSubject().getValue();
184 6532 leinfelder
            if (session.getSubjectInfo() != null) {
185
                List<Group> groupList = session.getSubjectInfo().getGroupList();
186 6475 jones
                if (groupList != null) {
187
                    groupnames = new String[groupList.size()];
188
                    for (int i = 0; i > groupList.size(); i++) {
189
                        groupnames[i] = groupList.get(i).getGroupName();
190
                    }
191
                }
192
            }
193
        }
194
195
        // do we have a valid pid?
196
        if (pid == null || pid.getValue().trim().equals("")) {
197 6610 cjones
            throw new ServiceFailure("1350", "The provided identifier was invalid.");
198 6475 jones
        }
199
200
        // check for the existing identifier
201
        try {
202
            localId = IdentifierManager.getInstance().getLocalId(pid.getValue());
203
        } catch (McdbDocNotFoundException e) {
204 6610 cjones
            throw new NotFound("1340", "The object with the provided " + "identifier was not found.");
205 6475 jones
        }
206
207
        // does the subject have DELETE (a D1 CHANGE_PERMISSION level) priveleges on the pid?
208
        allowed = isAuthorized(session, pid, Permission.CHANGE_PERMISSION);
209 6610 cjones
210 6475 jones
211
        if (allowed) {
212
            try {
213
                // delete the document
214
                DocumentImpl.delete(localId, username, groupnames, null);
215 6542 leinfelder
                EventLog.getInstance().log(request.getRemoteAddr(), request.getHeader("User-Agent"), username, localId, Event.DELETE.xmlValue());
216 6475 jones
217
            } catch (McdbDocNotFoundException e) {
218 6610 cjones
                throw new NotFound("1340", "The provided identifier was invalid.");
219 6475 jones
220
            } catch (SQLException e) {
221
                throw new ServiceFailure("1350", "There was a problem deleting the object." + "The error message was: " + e.getMessage());
222
223
            } catch (InsufficientKarmaException e) {
224
                throw new NotAuthorized("1320", "The provided identity does not have " + "permission to DELETE objects on the Member Node.");
225
226
            } catch (Exception e) { // for some reason DocumentImpl throws a general Exception
227
                throw new ServiceFailure("1350", "There was a problem deleting the object." + "The error message was: " + e.getMessage());
228
            }
229
230
        } else {
231
            throw new NotAuthorized("1320", "The provided identity does not have " + "permission to DELETE objects on the Member Node.");
232
        }
233
234
        return pid;
235 6250 cjones
    }
236
237 6475 jones
    /**
238
     * Updates an existing object by creating a new object identified by
239
     * newPid on the Member Node which explicitly obsoletes the object
240
     * identified by pid through appropriate changes to the SystemMetadata
241
     * of pid and newPid
242
     *
243
     * @param session - the Session object containing the credentials for the Subject
244
     * @param pid - The identifier of the object to be updated
245
     * @param object - the new object bytes
246
     * @param sysmeta - the new system metadata describing the object
247
     *
248
     * @return newPid - the identifier of the new object
249
     *
250
     * @throws InvalidToken
251
     * @throws ServiceFailure
252
     * @throws NotAuthorized
253
     * @throws NotFound
254
     * @throws NotImplemented
255
     * @throws IdentifierNotUnique
256
     * @throws UnsupportedType
257
     * @throws InsufficientResources
258
     * @throws InvalidSystemMetadata
259
     * @throws InvalidRequest
260
     */
261
    @Override
262 6575 cjones
    public Identifier update(Session session, Identifier pid, InputStream object,
263
        Identifier newPid, SystemMetadata sysmeta)
264
        throws InvalidToken, ServiceFailure, NotAuthorized, IdentifierNotUnique,
265
        UnsupportedType, InsufficientResources, NotFound,
266
        InvalidSystemMetadata, NotImplemented, InvalidRequest {
267 6250 cjones
268 6475 jones
        String localId = null;
269
        boolean allowed = false;
270
        boolean isScienceMetadata = false;
271
        Subject subject = session.getSubject();
272
273
        // do we have a valid pid?
274
        if (pid == null || pid.getValue().trim().equals("")) {
275
            throw new InvalidRequest("1202", "The provided identifier was invalid.");
276 6575 cjones
277 6475 jones
        }
278
279
        // check for the existing identifier
280
        try {
281
            localId = IdentifierManager.getInstance().getLocalId(pid.getValue());
282 6575 cjones
283 6475 jones
        } catch (McdbDocNotFoundException e) {
284 6575 cjones
            throw new InvalidRequest("1202", "The object with the provided " +
285
                "identifier was not found.");
286
287 6475 jones
        }
288 6518 leinfelder
289 6521 leinfelder
        // set the originating node
290
        NodeReference originMemberNode = this.getCapabilities().getIdentifier();
291
        sysmeta.setOriginMemberNode(originMemberNode);
292
293 6518 leinfelder
        // set the submitter to match the certificate
294
        sysmeta.setSubmitter(subject);
295 6525 leinfelder
        // set the dates
296
        Date now = Calendar.getInstance().getTime();
297 6575 cjones
        sysmeta.setDateSysMetadataModified(now);
298
        sysmeta.setDateUploaded(now);
299 6475 jones
300
        // does the subject have WRITE ( == update) priveleges on the pid?
301
        allowed = isAuthorized(session, pid, Permission.WRITE);
302
303
        if (allowed) {
304
305
            // get the existing system metadata for the object
306
            SystemMetadata existingSysMeta = getSystemMetadata(session, pid);
307
308
            // add the newPid to the obsoletedBy list for the existing sysmeta
309
            existingSysMeta.setObsoletedBy(newPid);
310
311
            // then update the existing system metadata
312
            updateSystemMetadata(existingSysMeta);
313
314
            // prep the new system metadata, add pid to the affected lists
315
            sysmeta.setObsoletes(pid);
316
            //sysmeta.addDerivedFrom(pid);
317
318
            isScienceMetadata = isScienceMetadata(sysmeta);
319
320
            // do we have XML metadata or a data object?
321
            if (isScienceMetadata) {
322
323
                // update the science metadata XML document
324
                // TODO: handle non-XML metadata/data documents (like netCDF)
325
                // TODO: don't put objects into memory using stream to string
326
                String objectAsXML = "";
327
                try {
328
                    objectAsXML = IOUtils.toString(object, "UTF-8");
329
                    localId = insertOrUpdateDocument(objectAsXML, newPid, session, "update");
330
                    // register the newPid and the generated localId
331
                    if (newPid != null) {
332
                        IdentifierManager.getInstance().createMapping(newPid.getValue(), localId);
333
334
                    }
335
336
                } catch (IOException e) {
337
                    String msg = "The Node is unable to create the object. " + "There was a problem converting the object to XML";
338
                    logMetacat.info(msg);
339
                    throw new ServiceFailure("1310", msg + ": " + e.getMessage());
340
341
                }
342
343
            } else {
344
345
                // update the data object
346
                localId = insertDataObject(object, newPid, session);
347
348
            }
349
350
            // and insert the new system metadata
351
            insertSystemMetadata(sysmeta);
352
353
            // log the update event
354 6542 leinfelder
            EventLog.getInstance().log(request.getRemoteAddr(), request.getHeader("User-Agent"), subject.getValue(), localId, Event.UPDATE.toString());
355 6475 jones
356
        } else {
357
            throw new NotAuthorized("1200", "The provided identity does not have " + "permission to UPDATE the object identified by " + pid.getValue()
358
                    + " on the Member Node.");
359
        }
360
361
        return newPid;
362 6250 cjones
    }
363 6254 cjones
364 6475 jones
    public Identifier create(Session session, Identifier pid, InputStream object, SystemMetadata sysmeta) throws InvalidToken, ServiceFailure, NotAuthorized,
365
            IdentifierNotUnique, UnsupportedType, InsufficientResources, InvalidSystemMetadata, NotImplemented, InvalidRequest {
366 6250 cjones
367 6575 cjones
      // check for null session
368 6530 leinfelder
        if (session == null) {
369 6575 cjones
          throw new InvalidToken("1110", "Session is required to WRITE to the Node.");
370 6530 leinfelder
        }
371 6518 leinfelder
        // set the submitter to match the certificate
372
        sysmeta.setSubmitter(session.getSubject());
373 6520 leinfelder
        // set the originating node
374
        NodeReference originMemberNode = this.getCapabilities().getIdentifier();
375
        sysmeta.setOriginMemberNode(originMemberNode);
376 6525 leinfelder
        // set the dates
377
        Date now = Calendar.getInstance().getTime();
378 6575 cjones
    sysmeta.setDateSysMetadataModified(now);
379
    sysmeta.setDateUploaded(now);
380 6518 leinfelder
        // call the shared impl
381 6475 jones
        return super.create(session, pid, object, sysmeta);
382
    }
383 6250 cjones
384 6475 jones
    /**
385
     * Called by a Coordinating Node to request that the Member Node create a
386
     * copy of the specified object by retrieving it from another Member
387
     * Node and storing it locally so that it can be made accessible to
388
     * the DataONE system.
389
     *
390
     * @param session - the Session object containing the credentials for the Subject
391
     * @param sysmeta - Copy of the CN held system metadata for the object
392
     * @param sourceNode - A reference to node from which the content should be
393
     *                     retrieved. The reference should be resolved by
394
     *                     checking the CN node registry.
395
     *
396
     * @return true if the replication succeeds
397
     *
398
     * @throws ServiceFailure
399
     * @throws NotAuthorized
400
     * @throws NotImplemented
401
     * @throws UnsupportedType
402
     * @throws InsufficientResources
403
     * @throws InvalidRequest
404
     */
405
    @Override
406 6528 cjones
    public boolean replicate(Session session, SystemMetadata sysmeta, NodeReference sourceNode)
407
        throws NotImplemented, ServiceFailure, NotAuthorized, InvalidRequest,
408
        InsufficientResources, UnsupportedType {
409 6250 cjones
410 6540 cjones
        logMetacat.info("MNodeService.replicate() called with parameters: \n" +
411
            "\tSession.Subject      = " + session.getSubject().getValue() + "\n" +
412
            "\tSystemMetadata       = " + sysmeta.toString()              + "\n" +
413
            "\tSource NodeReference ="  + sourceNode.getValue());
414
415 6475 jones
        boolean result = false;
416
417
        // TODO: check credentials
418
419
        // get the referenced object
420
        Identifier pid = sysmeta.getIdentifier();
421
422
        // get from the membernode
423
        // TODO: switch credentials for the server retrieval?
424
        MNode mn = D1Client.getMN(sourceNode);
425 6528 cjones
        CNode cn = D1Client.getCN();
426 6599 cjones
        long serialVersion = sysmeta.getSerialVersion().longValue();
427 6475 jones
        InputStream object = null;
428 6630 cjones
        Session thisNodeSession = null;
429 6552 leinfelder
430 6475 jones
        try {
431 6575 cjones
          // session should be null to use the default certificate location set in the Certificate manager
432 6630 cjones
            object = mn.getReplica(thisNodeSession, pid);
433 6540 cjones
            logMetacat.info("MNodeService.replicate() called for identifier " + pid.getValue());
434
435 6475 jones
        } catch (InvalidToken e) {
436
            e.printStackTrace();
437
            throw new ServiceFailure("2151", "Could not retrieve object to replicate (InvalidToken): " + e.getMessage());
438
        } catch (NotFound e) {
439
            e.printStackTrace();
440
            throw new ServiceFailure("2151", "Could not retrieve object to replicate (NotFound): " + e.getMessage());
441
        }
442
443
        // add it to local store
444
        Identifier retPid;
445
        try {
446 6575 cjones
          // skip the MN.create -- this mutates the system metadata and we dont want it to
447 6552 leinfelder
            retPid = super.create(session, pid, object, sysmeta);
448 6475 jones
            result = (retPid.getValue().equals(pid.getValue()));
449
        } catch (InvalidToken e) {
450
            e.printStackTrace();
451
            throw new ServiceFailure("2151", "Could not save object to local store (InvalidToken): " + e.getMessage());
452
        } catch (IdentifierNotUnique e) {
453
            e.printStackTrace();
454
            throw new ServiceFailure("2151", "Could not save object to local store (IdentifierNotUnique): " + e.getMessage());
455
        } catch (InvalidSystemMetadata e) {
456
            e.printStackTrace();
457
            throw new ServiceFailure("2151", "Could not save object to local store (InvalidSystemMetadata): " + e.getMessage());
458
        }
459
460 6528 cjones
        try {
461 6575 cjones
          // call the CN as the MN to set the replication status
462 6599 cjones
            cn.setReplicationStatus(null, pid, sourceNode, ReplicationStatus.COMPLETED, serialVersion);
463 6528 cjones
464
        } catch (InvalidToken e) {
465
            // TODO Auto-generated catch block
466
            e.printStackTrace();
467
        } catch (NotFound e) {
468
            // TODO Auto-generated catch block
469
            e.printStackTrace();
470 6622 leinfelder
        } catch (VersionMismatch e) {
471
			// TODO Auto-generated catch block
472
			e.printStackTrace();
473
		}
474 6475 jones
        return result;
475
476 6250 cjones
    }
477 6179 cjones
478 6475 jones
    /**
479
     * This method provides a lighter weight mechanism than
480
     * MN_read.getSystemMetadata() for a client to determine basic
481
     * properties of the referenced object.
482
     *
483
     * @param session - the Session object containing the credentials for the Subject
484
     * @param pid - the identifier of the object to be described
485
     *
486
     * @return describeResponse - A set of values providing a basic description
487
     *                            of the object.
488
     *
489
     * @throws InvalidToken
490
     * @throws ServiceFailure
491
     * @throws NotAuthorized
492
     * @throws NotFound
493
     * @throws NotImplemented
494
     * @throws InvalidRequest
495
     */
496
    @Override
497 6610 cjones
    public DescribeResponse describe(Session session, Identifier pid)
498
        throws InvalidToken, ServiceFailure, NotAuthorized, NotFound, NotImplemented {
499 6251 cjones
500 6575 cjones
      // get system metadata and construct the describe response
501 6475 jones
        SystemMetadata sysmeta = getSystemMetadata(session, pid);
502 6561 leinfelder
        DescribeResponse describeResponse = new DescribeResponse(sysmeta.getFormatId(), sysmeta.getSize(), sysmeta.getDateSysMetadataModified(),
503 6475 jones
                sysmeta.getChecksum());
504
505
        return describeResponse;
506
507 6259 cjones
    }
508 6258 cjones
509 6475 jones
    /**
510
     * Return the object identified by the given object identifier
511
     *
512
     * @param session - the Session object containing the credentials for the Subject
513
     * @param pid - the object identifier for the given object
514
     *
515
     * @return inputStream - the input stream of the given object
516
     *
517
     * @throws InvalidToken
518
     * @throws ServiceFailure
519
     * @throws NotAuthorized
520
     * @throws InvalidRequest
521
     * @throws NotImplemented
522
     */
523
    @Override
524 6610 cjones
    public InputStream get(Session session, Identifier pid)
525
    throws InvalidToken, ServiceFailure, NotAuthorized, NotFound, NotImplemented {
526 6258 cjones
527 6475 jones
        return super.get(session, pid);
528 6258 cjones
529 6259 cjones
    }
530 6258 cjones
531 6475 jones
    /**
532
     * Returns a Checksum for the specified object using an accepted hashing algorithm
533
     *
534
     * @param session - the Session object containing the credentials for the Subject
535
     * @param pid - the object identifier for the given object
536
     * @param algorithm -  the name of an algorithm that will be used to compute
537
     *                     a checksum of the bytes of the object
538
     *
539
     * @return checksum - the checksum of the given object
540
     *
541
     * @throws InvalidToken
542
     * @throws ServiceFailure
543
     * @throws NotAuthorized
544
     * @throws NotFound
545
     * @throws InvalidRequest
546
     * @throws NotImplemented
547
     */
548
    @Override
549 6610 cjones
    public Checksum getChecksum(Session session, Identifier pid, String algorithm)
550
        throws InvalidToken, ServiceFailure, NotAuthorized, NotFound,
551
        InvalidRequest, NotImplemented {
552 6258 cjones
553 6475 jones
        Checksum checksum = null;
554
555
        InputStream inputStream = get(session, pid);
556
557 6259 cjones
        try {
558 6475 jones
            checksum = ChecksumUtil.checksum(inputStream, algorithm);
559
560
        } catch (NoSuchAlgorithmException e) {
561
            throw new ServiceFailure("1410", "The checksum for the object specified by " + pid.getValue() + "could not be returned due to an internal error: "
562
                    + e.getMessage());
563 6259 cjones
        } catch (IOException e) {
564 6475 jones
            throw new ServiceFailure("1410", "The checksum for the object specified by " + pid.getValue() + "could not be returned due to an internal error: "
565
                    + e.getMessage());
566 6259 cjones
        }
567 6382 cjones
568 6475 jones
        if (checksum == null) {
569
            throw new ServiceFailure("1410", "The checksum for the object specified by " + pid.getValue() + "could not be returned.");
570
        }
571 6258 cjones
572 6475 jones
        return checksum;
573 6259 cjones
    }
574 6179 cjones
575 6475 jones
    /**
576
     * Return the system metadata for a given object
577
     *
578
     * @param session - the Session object containing the credentials for the Subject
579
     * @param pid - the object identifier for the given object
580
     *
581
     * @return inputStream - the input stream of the given system metadata object
582
     *
583
     * @throws InvalidToken
584
     * @throws ServiceFailure
585
     * @throws NotAuthorized
586
     * @throws NotFound
587
     * @throws InvalidRequest
588
     * @throws NotImplemented
589
     */
590
    @Override
591 6610 cjones
    public SystemMetadata getSystemMetadata(Session session, Identifier pid)
592
        throws InvalidToken, ServiceFailure, NotAuthorized, NotFound,
593
        NotImplemented {
594 6341 leinfelder
595 6475 jones
        return super.getSystemMetadata(session, pid);
596
    }
597 6341 leinfelder
598 6475 jones
    /**
599
     * Retrieve the list of objects present on the MN that match the calling parameters
600
     *
601
     * @param session - the Session object containing the credentials for the Subject
602
     * @param startTime - Specifies the beginning of the time range from which
603
     *                    to return object (>=)
604
     * @param endTime - Specifies the beginning of the time range from which
605
     *                  to return object (>=)
606
     * @param objectFormat - Restrict results to the specified object format
607
     * @param replicaStatus - Indicates if replicated objects should be returned in the list
608
     * @param start - The zero-based index of the first value, relative to the
609
     *                first record of the resultset that matches the parameters.
610
     * @param count - The maximum number of entries that should be returned in
611
     *                the response. The Member Node may return less entries
612
     *                than specified in this value.
613
     *
614
     * @return objectList - the list of objects matching the criteria
615
     *
616
     * @throws InvalidToken
617
     * @throws ServiceFailure
618
     * @throws NotAuthorized
619
     * @throws InvalidRequest
620
     * @throws NotImplemented
621
     */
622
    @Override
623
    public ObjectList listObjects(Session session, Date startTime, Date endTime, ObjectFormatIdentifier objectFormatId, Boolean replicaStatus, Integer start,
624
            Integer count) throws NotAuthorized, InvalidRequest, NotImplemented, ServiceFailure, InvalidToken {
625 6179 cjones
626 6475 jones
        ObjectList objectList = null;
627 6332 leinfelder
628 6475 jones
        try {
629
            objectList = IdentifierManager.getInstance().querySystemMetadata(startTime, endTime, objectFormatId, replicaStatus, start, count);
630
        } catch (Exception e) {
631
            throw new ServiceFailure("1580", "Error querying system metadata: " + e.getMessage());
632
        }
633 6332 leinfelder
634 6475 jones
        return objectList;
635 6229 cjones
    }
636 6179 cjones
637 6475 jones
    /**
638 6476 jones
     * Return a description of the node's capabilities and services.
639 6475 jones
     *
640
     * @return node - the technical capabilities of the Member Node
641
     *
642
     * @throws ServiceFailure
643
     * @throws NotAuthorized
644
     * @throws InvalidRequest
645
     * @throws NotImplemented
646
     */
647
    @Override
648 6610 cjones
    public Node getCapabilities()
649
        throws NotImplemented, ServiceFailure {
650 6179 cjones
651 6475 jones
        String nodeName = null;
652
        String nodeId = null;
653 6492 jones
        String subject = null;
654 6475 jones
        String nodeDesc = null;
655 6476 jones
        String nodeTypeString = null;
656
        NodeType nodeType = null;
657 6475 jones
        String mnCoreServiceVersion = null;
658
        String mnReadServiceVersion = null;
659
        String mnAuthorizationServiceVersion = null;
660
        String mnStorageServiceVersion = null;
661
        String mnReplicationServiceVersion = null;
662 6179 cjones
663 6475 jones
        boolean nodeSynchronize = false;
664
        boolean nodeReplicate = false;
665
        boolean mnCoreServiceAvailable = false;
666
        boolean mnReadServiceAvailable = false;
667
        boolean mnAuthorizationServiceAvailable = false;
668
        boolean mnStorageServiceAvailable = false;
669
        boolean mnReplicationServiceAvailable = false;
670 6179 cjones
671 6475 jones
        try {
672
            // get the properties of the node based on configuration information
673 6492 jones
            nodeName = PropertyService.getProperty("dataone.nodeName");
674 6475 jones
            nodeId = PropertyService.getProperty("dataone.memberNodeId");
675 6492 jones
            subject = PropertyService.getProperty("dataone.subject");
676 6475 jones
            nodeDesc = PropertyService.getProperty("dataone.nodeDescription");
677 6476 jones
            nodeTypeString = PropertyService.getProperty("dataone.nodeType");
678
            nodeType = NodeType.convert(nodeTypeString);
679 6475 jones
            nodeSynchronize = new Boolean(PropertyService.getProperty("dataone.nodeSynchronize")).booleanValue();
680
            nodeReplicate = new Boolean(PropertyService.getProperty("dataone.nodeReplicate")).booleanValue();
681
682
            mnCoreServiceVersion = PropertyService.getProperty("dataone.mnCore.serviceVersion");
683
            mnReadServiceVersion = PropertyService.getProperty("dataone.mnRead.serviceVersion");
684
            mnAuthorizationServiceVersion = PropertyService.getProperty("dataone.mnAuthorization.serviceVersion");
685
            mnStorageServiceVersion = PropertyService.getProperty("dataone.mnStorage.serviceVersion");
686
            mnReplicationServiceVersion = PropertyService.getProperty("dataone.mnReplication.serviceVersion");
687
688
            mnCoreServiceAvailable = new Boolean(PropertyService.getProperty("dataone.mnCore.serviceAvailable")).booleanValue();
689
            mnReadServiceAvailable = new Boolean(PropertyService.getProperty("dataone.mnRead.serviceAvailable")).booleanValue();
690
            mnAuthorizationServiceAvailable = new Boolean(PropertyService.getProperty("dataone.mnAuthorization.serviceAvailable")).booleanValue();
691
            mnStorageServiceAvailable = new Boolean(PropertyService.getProperty("dataone.mnStorage.serviceAvailable")).booleanValue();
692
            mnReplicationServiceAvailable = new Boolean(PropertyService.getProperty("dataone.mnReplication.serviceAvailable")).booleanValue();
693
694 6476 jones
            // Set the properties of the node based on configuration information and
695
            // calls to current status methods
696 6542 leinfelder
            String serviceName = SystemUtil.getContextURL() + "/" + PropertyService.getProperty("dataone.serviceName");
697 6476 jones
            Node node = new Node();
698 6542 leinfelder
            node.setBaseURL(serviceName + "/" + nodeTypeString);
699 6476 jones
            node.setDescription(nodeDesc);
700 6475 jones
701 6476 jones
            // set the node's health information
702
            node.setState(NodeState.UP);
703
704
            // set the ping response to the current value
705
            Ping canPing = new Ping();
706
            canPing.setSuccess(false);
707
            try {
708
                canPing.setSuccess(ping());
709
            } catch (InsufficientResources e) {
710
                e.printStackTrace();
711
            }
712 6610 cjones
713 6476 jones
            node.setPing(canPing);
714 6475 jones
715 6476 jones
            NodeReference identifier = new NodeReference();
716
            identifier.setValue(nodeId);
717
            node.setIdentifier(identifier);
718 6492 jones
            Subject s = new Subject();
719
            s.setValue(subject);
720
            node.addSubject(s);
721 6476 jones
            node.setName(nodeName);
722
            node.setReplicate(nodeReplicate);
723
            node.setSynchronize(nodeSynchronize);
724 6475 jones
725 6476 jones
            // services: MNAuthorization, MNCore, MNRead, MNReplication, MNStorage
726
            Services services = new Services();
727 6475 jones
728 6476 jones
            Service sMNCore = new Service();
729
            sMNCore.setName("MNCore");
730
            sMNCore.setVersion(mnCoreServiceVersion);
731
            sMNCore.setAvailable(mnCoreServiceAvailable);
732 6475 jones
733 6476 jones
            Service sMNRead = new Service();
734
            sMNRead.setName("MNRead");
735
            sMNRead.setVersion(mnReadServiceVersion);
736
            sMNRead.setAvailable(mnReadServiceAvailable);
737 6475 jones
738 6476 jones
            Service sMNAuthorization = new Service();
739
            sMNAuthorization.setName("MNAuthorization");
740
            sMNAuthorization.setVersion(mnAuthorizationServiceVersion);
741
            sMNAuthorization.setAvailable(mnAuthorizationServiceAvailable);
742 6475 jones
743 6476 jones
            Service sMNStorage = new Service();
744
            sMNStorage.setName("MNStorage");
745
            sMNStorage.setVersion(mnStorageServiceVersion);
746
            sMNStorage.setAvailable(mnStorageServiceAvailable);
747 6475 jones
748 6476 jones
            Service sMNReplication = new Service();
749
            sMNReplication.setName("MNReplication");
750
            sMNReplication.setVersion(mnReplicationServiceVersion);
751
            sMNReplication.setAvailable(mnReplicationServiceAvailable);
752 6475 jones
753 6476 jones
            services.addService(sMNRead);
754
            services.addService(sMNCore);
755
            services.addService(sMNAuthorization);
756
            services.addService(sMNStorage);
757
            services.addService(sMNReplication);
758
            node.setServices(services);
759 6475 jones
760 6476 jones
            // TODO: Allow the metacat admin to determine the schedule
761
            // Set the schedule for synchronization
762
            Synchronization synchronization = new Synchronization();
763
            Schedule schedule = new Schedule();
764
            Date now = new Date();
765
            schedule.setYear("*");
766
            schedule.setMon("*");
767
            schedule.setMday("*");
768 6512 jones
            schedule.setWday("?");
769 6476 jones
            schedule.setHour("*");
770 6512 jones
            schedule.setMin("0/3");
771
            schedule.setSec("10");
772 6476 jones
            synchronization.setSchedule(schedule);
773
            synchronization.setLastHarvested(now);
774
            synchronization.setLastCompleteHarvest(now);
775
            node.setSynchronization(synchronization);
776 6475 jones
777 6476 jones
            node.setType(nodeType);
778
            return node;
779 6475 jones
780 6476 jones
        } catch (PropertyNotFoundException pnfe) {
781
            String msg = "MNodeService.getCapabilities(): " + "property not found: " + pnfe.getMessage();
782
            logMetacat.error(msg);
783
            throw new ServiceFailure("2162", msg);
784
        }
785 6228 cjones
    }
786 6179 cjones
787 6475 jones
    /**
788
     * Returns the number of operations that have been serviced by the node
789
     * over time periods of one and 24 hours.
790
     *
791
     * @param session - the Session object containing the credentials for the Subject
792
     * @param period - An ISO8601 compatible DateTime range specifying the time
793
     *                 range for which to return operation statistics.
794
     * @param requestor - Limit to operations performed by given requestor identity.
795
     * @param event -  Enumerated value indicating the type of event being examined
796
     * @param format - Limit to events involving objects of the specified format
797
     *
798
     * @return the desired log records
799
     *
800
     * @throws InvalidToken
801
     * @throws ServiceFailure
802
     * @throws NotAuthorized
803
     * @throws InvalidRequest
804
     * @throws NotImplemented
805
     */
806 6610 cjones
    public MonitorList getOperationStatistics(Session session, Date startTime,
807
        Date endTime, Subject requestor, Event event, ObjectFormatIdentifier formatId)
808
        throws NotImplemented, ServiceFailure, NotAuthorized, InsufficientResources, UnsupportedType {
809 6179 cjones
810 6475 jones
        MonitorList monitorList = new MonitorList();
811 6179 cjones
812 6475 jones
        try {
813 6179 cjones
814 6475 jones
            // get log records first
815
            Log logs = getLogRecords(session, startTime, endTime, event, 0, null);
816 6179 cjones
817 6475 jones
            // TODO: aggregate by day or hour -- needs clarification
818
            int count = 1;
819
            for (LogEntry logEntry : logs.getLogEntryList()) {
820
                Identifier pid = logEntry.getIdentifier();
821
                Date logDate = logEntry.getDateLogged();
822
                // if we are filtering by format
823
                if (formatId != null) {
824
                    SystemMetadata sysmeta = IdentifierManager.getInstance().getSystemMetadata(pid.getValue());
825 6561 leinfelder
                    if (!sysmeta.getFormatId().getValue().equals(formatId.getValue())) {
826 6475 jones
                        // does not match
827
                        continue;
828
                    }
829
                }
830
                MonitorInfo item = new MonitorInfo();
831
                item.setCount(count);
832
                item.setDate(new java.sql.Date(logDate.getTime()));
833
                monitorList.addMonitorInfo(item);
834 6179 cjones
835 6475 jones
            }
836
        } catch (Exception e) {
837
            e.printStackTrace();
838
            throw new ServiceFailure("2081", "Could not retrieve statistics: " + e.getMessage());
839
        }
840 6345 cjones
841 6475 jones
        return monitorList;
842 6345 cjones
843 6340 cjones
    }
844
845 6475 jones
    /**
846
     * Low level “are you alive” operation. A valid ping response is
847
     * indicated by a HTTP status of 200.
848
     *
849
     * @return true if the service is alive
850
     *
851
     * @throws InvalidToken
852
     * @throws ServiceFailure
853
     * @throws NotImplemented
854
     */
855
    @Override
856 6610 cjones
    public boolean ping()
857
        throws NotImplemented, ServiceFailure, InsufficientResources {
858 6475 jones
859
        // test if we can get a database connection
860
        boolean alive = false;
861
        int serialNumber = -1;
862
        DBConnection dbConn = null;
863
        try {
864
            dbConn = DBConnectionPool.getDBConnection("MNodeService.ping");
865
            serialNumber = dbConn.getCheckOutSerialNumber();
866
            alive = true;
867
        } catch (SQLException e) {
868
            return alive;
869
        } finally {
870
            // Return the database connection
871
            DBConnectionPool.returnDBConnection(dbConn, serialNumber);
872
        }
873
874
        return alive;
875 6351 cjones
    }
876
877 6475 jones
    /**
878
     * A callback method used by a CN to indicate to a MN that it cannot
879
     * complete synchronization of the science metadata identified by pid.  Log
880
     * the event in the metacat event log.
881
     *
882
     * @param session
883
     * @param syncFailed
884
     *
885
     * @throws ServiceFailure
886
     * @throws NotAuthorized
887
     * @throws NotImplemented
888
     */
889
    @Override
890 6610 cjones
    public void synchronizationFailed(Session session, SynchronizationFailed syncFailed)
891
        throws NotImplemented, ServiceFailure, NotAuthorized {
892 6179 cjones
893 6475 jones
        String localId;
894 6331 leinfelder
895 6475 jones
        try {
896
            localId = IdentifierManager.getInstance().getLocalId(syncFailed.getPid());
897
        } catch (McdbDocNotFoundException e) {
898 6610 cjones
            throw new ServiceFailure("2161", "The identifier specified by " + syncFailed.getPid() + " was not found on this node.");
899 6179 cjones
900 6475 jones
        }
901
        // TODO: update the CN URL below when the CNRead.SynchronizationFailed
902
        // method is changed to include the URL as a parameter
903
        logMetacat.debug("Synchronization for the object identified by " + syncFailed.getPid() + " failed from " + syncFailed.getNodeId()
904
                + " Logging the event to the Metacat EventLog as a 'syncFailed' event.");
905
        // TODO: use the event type enum when the SYNCHRONIZATION_FAILED event is added
906 6532 leinfelder
        String principal = Constants.SUBJECT_PUBLIC;
907 6506 leinfelder
        if (session != null && session.getSubject() != null) {
908 6575 cjones
          principal = session.getSubject().getValue();
909 6506 leinfelder
        }
910
        try {
911 6575 cjones
          EventLog.getInstance().log(request.getRemoteAddr(), request.getHeader("User-Agent"), principal, localId, "synchronization_failed");
912 6506 leinfelder
        } catch (Exception e) {
913
            throw new ServiceFailure("2161", "Could not log the error for: " + syncFailed.getPid());
914 6575 cjones
    }
915 6475 jones
        //EventLog.getInstance().log("CN URL WILL GO HERE",
916
        //  session.getSubject().getValue(), localId, Event.SYNCHRONIZATION_FAILED);
917 6179 cjones
918 6260 cjones
    }
919
920 6475 jones
    /**
921
     * Essentially a get() but with different logging behavior
922
     */
923
    @Override
924 6540 cjones
    public InputStream getReplica(Session session, Identifier pid)
925 6610 cjones
        throws NotAuthorized, NotImplemented, ServiceFailure {
926 6179 cjones
927 6540 cjones
        logMetacat.info("MNodeService.getReplica() called.");
928
929 6631 cjones
        logMetacat.info("MNodeService.getReplica() called with parameters: \n" +
930
             "\tSession.Subject      = " + session.getSubject().getValue() + "\n" +
931
             "\tIdentifier           = " + pid.getValue());
932
933 6475 jones
        InputStream inputStream = null; // bytes to be returned
934
        handler = new MetacatHandler(new Timer());
935
        boolean allowed = false;
936
        String localId; // the metacat docid for the pid
937 6179 cjones
938 6475 jones
        // get the local docid from Metacat
939
        try {
940
            localId = IdentifierManager.getInstance().getLocalId(pid.getValue());
941
        } catch (McdbDocNotFoundException e) {
942 6610 cjones
            throw new ServiceFailure("2181", "The object specified by " +
943
                    pid.getValue() + " does not exist at this node.");
944
945 6475 jones
        }
946 6234 cjones
947 6552 leinfelder
        Subject targetNodeSubject = session.getSubject();
948 6185 leinfelder
949 6552 leinfelder
        // check for authorization to replicate, null session to act as this source MN
950 6610 cjones
        try {
951
            allowed = D1Client.getCN().isNodeAuthorized(null, targetNodeSubject, pid, Permission.REPLICATE);
952
        } catch (InvalidToken e1) {
953
            throw new ServiceFailure("2181", "Could not determine if node is authorized: "
954
                + e1.getMessage());
955
956
        } catch (NotFound e1) {
957
            throw new ServiceFailure("2181", "Could not determine if node is authorized: "
958
                    + e1.getMessage());
959 6384 cjones
960 6610 cjones
        } catch (InvalidRequest e1) {
961
            throw new ServiceFailure("2181", "Could not determine if node is authorized: "
962
                    + e1.getMessage());
963
964
        }
965
966 6540 cjones
        logMetacat.info("Called D1Client.isNodeAuthorized(). Allowed = " + allowed +
967
            " for identifier " + pid.getValue());
968
969 6475 jones
        // if the person is authorized, perform the read
970
        if (allowed) {
971
            try {
972
                inputStream = handler.read(localId);
973
            } catch (Exception e) {
974 6610 cjones
                throw new ServiceFailure("1020", "The object specified by " +
975
                    pid.getValue() + "could not be returned due to error: " + e.getMessage());
976 6475 jones
            }
977
        }
978 6384 cjones
979 6475 jones
        // if we fail to set the input stream
980
        if (inputStream == null) {
981 6610 cjones
            throw new ServiceFailure("2181", "The object specified by " +
982
                pid.getValue() + "does not exist at this node.");
983 6475 jones
        }
984
985
        // log the replica event
986
        String principal = null;
987
        if (session.getSubject() != null) {
988
            principal = session.getSubject().getValue();
989
        }
990 6576 cjones
        EventLog.getInstance().log(request.getRemoteAddr(),
991
            request.getHeader("User-Agent"), principal, localId, "replicate");
992 6475 jones
993
        return inputStream;
994
    }
995
996 6573 cjones
    /**
997
     * Set the access policy
998
     */
999
    @Deprecated
1000
    @Override
1001
    public boolean setAccessPolicy(Session session, Identifier pid,
1002
        AccessPolicy policy)
1003
        throws InvalidToken, ServiceFailure, NotFound, NotAuthorized,
1004
        NotImplemented, InvalidRequest {
1005
1006
        throw new NotImplemented("4401", "This method is deprecated for Member Nodes.");
1007
1008
    }
1009
1010 6599 cjones
    /**
1011 6600 cjones
     * A method to notify the Member Node that the authoritative copy of
1012 6599 cjones
     * system metadata on the Coordinating Nodes has changed.
1013
     *
1014
     * @param session   Session information that contains the identity of the
1015
     *                  calling user as retrieved from the X.509 certificate
1016
     *                  which must be traceable to the CILogon service.
1017
     * @param serialVersion   The serialVersion of the system metadata
1018
     * @param dateSysMetaLastModified  The time stamp for when the system metadata was changed
1019
     * @throws NotImplemented
1020
     * @throws ServiceFailure
1021
     * @throws NotAuthorized
1022
     * @throws InvalidRequest
1023
     * @throws InvalidToken
1024
     */
1025
    public void systemMetadataChanged(Session session, Identifier pid,
1026
        long serialVersion, Date dateSysMetaLastModified)
1027
        throws NotImplemented, ServiceFailure, NotAuthorized, InvalidRequest,
1028
        InvalidToken {
1029
1030 6600 cjones
        SystemMetadata currentLocalSysMeta = null;
1031
        SystemMetadata newSysMeta = null;
1032
        CNode cn = D1Client.getCN();
1033
        NodeList nodeList = null;
1034
        Subject callingSubject = null;
1035
        boolean allowed = false;
1036
1037
        // are we allowed to call this?
1038
        callingSubject = session.getSubject();
1039
        nodeList = cn.listNodes();
1040
1041
        for(Node node : nodeList.getNodeList()) {
1042
            // must be a CN
1043
            if ( node.getType().equals(NodeType.CN)) {
1044
               List<Subject> subjectList = node.getSubjectList();
1045
               // the calling subject must be in the subject list
1046
               if ( subjectList.contains(callingSubject)) {
1047
                   allowed = true;
1048
1049
               }
1050
1051
            }
1052
        }
1053
1054
        if (!allowed ) {
1055
            String msg = "The subject identified by " + callingSubject.getValue() +
1056
              " is not authorized to call this service.";
1057
            throw new NotAuthorized("1331", msg);
1058
1059
        }
1060
1061
        // compare what we have locally to what is sent in the change notification
1062
        try {
1063
            currentLocalSysMeta =
1064
                IdentifierManager.getInstance().getSystemMetadata(pid.getValue());
1065
1066
        } catch (McdbDocNotFoundException e) {
1067
            String msg = "SystemMetadata for pid " + pid.getValue() +
1068
              " cpouldn't be updated because it couldn't be found locally: " +
1069
              e.getMessage();
1070
            logMetacat.warn(msg);
1071
1072
        }
1073
1074
        if (currentLocalSysMeta.getSerialVersion().longValue() < serialVersion ) {
1075
            try {
1076
                newSysMeta = cn.getSystemMetadata(null, pid);
1077
            } catch (NotFound e) {
1078
                // huh? you just said you had it
1079
                logMetacat.error("On updating the local copy of system metadata " +
1080
                    "for pid " + pid.getValue() +", the CN reports it is not found." +
1081
                    " The error message was: " + e.getMessage());
1082
1083
            }
1084
            // update the local copy of system metadata for the pid
1085
            try {
1086
                IdentifierManager.getInstance().updateSystemMetadata(newSysMeta);
1087
                logMetacat.info("Updated local copy of system metadata for pid " +
1088
                    pid.getValue() + " after change notification from the CN.");
1089
1090
            } catch (McdbDocNotFoundException e) {
1091
                String msg = "SystemMetadata for pid " + pid.getValue() +
1092
                  " cpouldn't be updated because it couldn't be found: " +
1093
                  e.getMessage();
1094
                logMetacat.warn(msg);
1095
1096
            }
1097
        }
1098
1099 6599 cjones
    }
1100
1101 6179 cjones
}