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.ByteArrayInputStream;
27
import java.io.ByteArrayOutputStream;
28
import java.io.File;
29
import java.io.FileInputStream;
30
import java.io.FileOutputStream;
31
import java.io.IOException;
32
import java.io.InputStream;
33
import java.io.InputStreamReader;
34
import java.io.OutputStreamWriter;
35
import java.io.UnsupportedEncodingException;
36
import java.io.Writer;
37
import java.math.BigInteger;
38
import java.net.URISyntaxException;
39
import java.security.NoSuchAlgorithmException;
40
import java.sql.SQLException;
41
import java.util.ArrayList;
42
import java.util.Calendar;
43
import java.util.Date;
44
import java.util.HashMap;
45
import java.util.HashSet;
46
import java.util.Hashtable;
47
import java.util.List;
48
import java.util.Map;
49
import java.util.Set;
50
import java.util.Timer;
51
import java.util.UUID;
52
import java.util.Vector;
53

    
54
import javax.servlet.http.HttpServletRequest;
55

    
56
import org.apache.commons.io.IOUtils;
57
import org.apache.log4j.Logger;
58
import org.dataone.client.v2.CNode;
59
import org.dataone.client.v2.itk.D1Client;
60
import org.dataone.client.v2.MNode;
61
import org.dataone.client.v2.formats.ObjectFormatCache;
62
import org.dataone.client.auth.CertificateManager;
63
import org.dataone.client.v2.formats.ObjectFormatInfo;
64
import org.dataone.configuration.Settings;
65
import org.dataone.ore.ResourceMapFactory;
66
import org.dataone.service.exceptions.BaseException;
67
import org.dataone.service.exceptions.IdentifierNotUnique;
68
import org.dataone.service.exceptions.InsufficientResources;
69
import org.dataone.service.exceptions.InvalidRequest;
70
import org.dataone.service.exceptions.InvalidSystemMetadata;
71
import org.dataone.service.exceptions.InvalidToken;
72
import org.dataone.service.exceptions.NotAuthorized;
73
import org.dataone.service.exceptions.NotFound;
74
import org.dataone.service.exceptions.NotImplemented;
75
import org.dataone.service.exceptions.ServiceFailure;
76
import org.dataone.service.exceptions.SynchronizationFailed;
77
import org.dataone.service.exceptions.UnsupportedType;
78
import org.dataone.service.mn.tier1.v2.MNCore;
79
import org.dataone.service.mn.tier1.v2.MNRead;
80
import org.dataone.service.mn.tier2.v2.MNAuthorization;
81
import org.dataone.service.mn.tier3.v2.MNStorage;
82
import org.dataone.service.mn.tier4.v2.MNReplication;
83
import org.dataone.service.mn.v2.MNPackage;
84
import org.dataone.service.mn.v2.MNQuery;
85
import org.dataone.service.mn.v2.MNView;
86
import org.dataone.service.types.v1.AccessRule;
87
import org.dataone.service.types.v1.Checksum;
88
import org.dataone.service.types.v1.DescribeResponse;
89
import org.dataone.service.types.v1.Event;
90
import org.dataone.service.types.v1.Identifier;
91
import org.dataone.service.types.v2.Log;
92
import org.dataone.service.types.v2.LogEntry;
93
import org.dataone.service.types.v2.OptionList;
94
import org.dataone.service.types.v1.MonitorInfo;
95
import org.dataone.service.types.v1.MonitorList;
96
import org.dataone.service.types.v2.Node;
97
import org.dataone.service.types.v2.NodeList;
98
import org.dataone.service.types.v1.NodeReference;
99
import org.dataone.service.types.v1.NodeState;
100
import org.dataone.service.types.v1.NodeType;
101
import org.dataone.service.types.v2.ObjectFormat;
102
import org.dataone.service.types.v1.ObjectFormatIdentifier;
103
import org.dataone.service.types.v1.ObjectList;
104
import org.dataone.service.types.v1.Permission;
105
import org.dataone.service.types.v1.Ping;
106
import org.dataone.service.types.v1.ReplicationStatus;
107
import org.dataone.service.types.v1.Schedule;
108
import org.dataone.service.types.v1.Service;
109
import org.dataone.service.types.v1.Services;
110
import org.dataone.service.types.v1.Session;
111
import org.dataone.service.types.v1.Subject;
112
import org.dataone.service.types.v1.Synchronization;
113
import org.dataone.service.types.v2.SystemMetadata;
114
import org.dataone.service.types.v1.util.AuthUtils;
115
import org.dataone.service.types.v1.util.ChecksumUtil;
116
import org.dataone.service.types.v1_1.QueryEngineDescription;
117
import org.dataone.service.types.v1_1.QueryEngineList;
118
import org.dataone.service.types.v1_1.QueryField;
119
import org.dataone.service.util.Constants;
120
import org.dataone.service.util.TypeMarshaller;
121
import org.dspace.foresite.OREException;
122
import org.dspace.foresite.OREParserException;
123
import org.dspace.foresite.ORESerialiserException;
124
import org.dspace.foresite.ResourceMap;
125
import org.ecoinformatics.datamanager.parser.DataPackage;
126
import org.ecoinformatics.datamanager.parser.Entity;
127
import org.ecoinformatics.datamanager.parser.generic.DataPackageParserInterface;
128
import org.ecoinformatics.datamanager.parser.generic.Eml200DataPackageParser;
129

    
130
import edu.ucsb.nceas.ezid.EZIDException;
131
import edu.ucsb.nceas.metacat.DBQuery;
132
import edu.ucsb.nceas.metacat.DBTransform;
133
import edu.ucsb.nceas.metacat.EventLog;
134
import edu.ucsb.nceas.metacat.IdentifierManager;
135
import edu.ucsb.nceas.metacat.McdbDocNotFoundException;
136
import edu.ucsb.nceas.metacat.MetaCatServlet;
137
import edu.ucsb.nceas.metacat.MetacatHandler;
138
import edu.ucsb.nceas.metacat.common.query.EnabledQueryEngines;
139
import edu.ucsb.nceas.metacat.common.query.stream.ContentTypeByteArrayInputStream;
140
import edu.ucsb.nceas.metacat.dataone.hazelcast.HazelcastService;
141
import edu.ucsb.nceas.metacat.index.MetacatSolrEngineDescriptionHandler;
142
import edu.ucsb.nceas.metacat.index.MetacatSolrIndex;
143
import edu.ucsb.nceas.metacat.properties.PropertyService;
144
import edu.ucsb.nceas.metacat.shared.MetacatUtilException;
145
import edu.ucsb.nceas.metacat.util.DeleteOnCloseFileInputStream;
146
import edu.ucsb.nceas.metacat.util.DocumentUtil;
147
import edu.ucsb.nceas.metacat.util.SkinUtil;
148
import edu.ucsb.nceas.metacat.util.SystemUtil;
149
import edu.ucsb.nceas.utilities.PropertyNotFoundException;
150
import edu.ucsb.nceas.utilities.XMLUtilities;
151
import edu.ucsb.nceas.utilities.export.HtmlToPdf;
152
import gov.loc.repository.bagit.Bag;
153
import gov.loc.repository.bagit.BagFactory;
154
import gov.loc.repository.bagit.writer.impl.ZipWriter;
155

    
156
/**
157
 * Represents Metacat's implementation of the DataONE Member Node 
158
 * service API. Methods implement the various MN* interfaces, and methods common
159
 * to both Member Node and Coordinating Node interfaces are found in the
160
 * D1NodeService base class.
161
 * 
162
 * Implements:
163
 * MNCore.ping()
164
 * MNCore.getLogRecords()
165
 * MNCore.getObjectStatistics()
166
 * MNCore.getOperationStatistics()
167
 * MNCore.getStatus()
168
 * MNCore.getCapabilities()
169
 * MNRead.get()
170
 * MNRead.getSystemMetadata()
171
 * MNRead.describe()
172
 * MNRead.getChecksum()
173
 * MNRead.listObjects()
174
 * MNRead.synchronizationFailed()
175
 * MNAuthorization.isAuthorized()
176
 * MNAuthorization.setAccessPolicy()
177
 * MNStorage.create()
178
 * MNStorage.update()
179
 * MNStorage.delete()
180
 * MNReplication.replicate()
181
 * 
182
 */
183
public class MNodeService extends D1NodeService 
184
    implements MNAuthorization, MNCore, MNRead, MNReplication, MNStorage, MNQuery, MNView, MNPackage {
185

    
186
    //private static final String PATHQUERY = "pathquery";
187
	public static final String UUID_SCHEME = "UUID";
188
	public static final String DOI_SCHEME = "DOI";
189
	private static final String UUID_PREFIX = "urn:uuid:";
190

    
191
	/* the logger instance */
192
    private Logger logMetacat = null;
193
    
194
    /* A reference to a remote Memeber Node */
195
    private MNode mn;
196
    
197
    /* A reference to a Coordinating Node */
198
    private CNode cn;
199

    
200

    
201
    /**
202
     * Singleton accessor to get an instance of MNodeService.
203
     * 
204
     * @return instance - the instance of MNodeService
205
     */
206
    public static MNodeService getInstance(HttpServletRequest request) {
207
        return new MNodeService(request);
208
    }
209

    
210
    /**
211
     * Constructor, private for singleton access
212
     */
213
    private MNodeService(HttpServletRequest request) {
214
        super(request);
215
        logMetacat = Logger.getLogger(MNodeService.class);
216
        
217
        // set the Member Node certificate file location
218
        CertificateManager.getInstance().setCertificateLocation(Settings.getConfiguration().getString("D1Client.certificate.file"));
219
    }
220

    
221
    /**
222
     * Deletes an object from the Member Node, where the object is either a 
223
     * data object or a science metadata object.
224
     * 
225
     * @param session - the Session object containing the credentials for the Subject
226
     * @param pid - The object identifier to be deleted
227
     * 
228
     * @return pid - the identifier of the object used for the deletion
229
     * 
230
     * @throws InvalidToken
231
     * @throws ServiceFailure
232
     * @throws NotAuthorized
233
     * @throws NotFound
234
     * @throws NotImplemented
235
     * @throws InvalidRequest
236
     */
237
    @Override
238
    public Identifier delete(Session session, Identifier pid) 
239
        throws InvalidToken, ServiceFailure, NotAuthorized, NotFound, NotImplemented {
240

    
241
    	// only admin of  the MN or the CN is allowed a full delete
242
        boolean allowed = false;
243
        allowed = isAdminAuthorized(session);
244
        
245
        //check if it is the authoritative member node
246
        if(!allowed) {
247
            allowed = isAuthoritativeMNodeAdmin(session, pid);
248
        }
249
        
250
        if (!allowed) { 
251
            throw new NotAuthorized("1320", "The provided identity does not have " + "permission to delete objects on the Node.");
252
        }
253
    	
254
    	// defer to superclass implementation
255
        return super.delete(session, pid);
256
    }
257

    
258
    /**
259
     * Updates an existing object by creating a new object identified by 
260
     * newPid on the Member Node which explicitly obsoletes the object 
261
     * identified by pid through appropriate changes to the SystemMetadata 
262
     * of pid and newPid
263
     * 
264
     * @param session - the Session object containing the credentials for the Subject
265
     * @param pid - The identifier of the object to be updated
266
     * @param object - the new object bytes
267
     * @param sysmeta - the new system metadata describing the object
268
     * 
269
     * @return newPid - the identifier of the new object
270
     * 
271
     * @throws InvalidToken
272
     * @throws ServiceFailure
273
     * @throws NotAuthorized
274
     * @throws NotFound
275
     * @throws NotImplemented
276
     * @throws IdentifierNotUnique
277
     * @throws UnsupportedType
278
     * @throws InsufficientResources
279
     * @throws InvalidSystemMetadata
280
     * @throws InvalidRequest
281
     */
282
    @Override
283
    public Identifier update(Session session, Identifier pid, InputStream object, 
284
        Identifier newPid, SystemMetadata sysmeta) 
285
        throws InvalidToken, ServiceFailure, NotAuthorized, IdentifierNotUnique, 
286
        UnsupportedType, InsufficientResources, NotFound, 
287
        InvalidSystemMetadata, NotImplemented, InvalidRequest {
288

    
289
        //transform a sid to a pid if it is applicable
290
        String serviceFailureCode = "1310";
291
        Identifier sid = getPIDForSID(pid, serviceFailureCode);
292
        if(sid != null) {
293
            pid = sid;
294
        }
295
        
296
        String localId = null;
297
        boolean allowed = false;
298
        boolean isScienceMetadata = false;
299
        
300
        if (session == null) {
301
        	throw new InvalidToken("1210", "No session has been provided");
302
        }
303
        Subject subject = session.getSubject();
304

    
305
        // verify the pid is valid format
306
        if (!isValidIdentifier(pid)) {
307
        	throw new InvalidRequest("1202", "The provided identifier is invalid.");
308
        }
309
        
310
        // verify the new pid is valid format
311
        if (!isValidIdentifier(newPid)) {
312
            throw new InvalidRequest("1202", "The provided identifier is invalid.");
313
        }
314
        
315
        // make sure that the newPid doesn't exists
316
        boolean idExists = true;
317
        try {
318
            idExists = IdentifierManager.getInstance().identifierExists(newPid.getValue());
319
        } catch (SQLException e) {
320
            throw new ServiceFailure("1310", 
321
                                    "The requested identifier " + newPid.getValue() +
322
                                    " couldn't be determined if it is unique since : "+e.getMessage());
323
        }
324
        if (idExists) {
325
                throw new IdentifierNotUnique("1220", 
326
                          "The requested identifier " + newPid.getValue() +
327
                          " is already used by another object and" +
328
                          "therefore can not be used for this object. Clients should choose" +
329
                          "a new identifier that is unique and retry the operation or " +
330
                          "use CN.reserveIdentifier() to reserve one.");
331
            
332
        }
333
        
334
       
335

    
336
        // check for the existing identifier
337
        try {
338
            localId = IdentifierManager.getInstance().getLocalId(pid.getValue());
339
            
340
        } catch (McdbDocNotFoundException e) {
341
            throw new InvalidRequest("1202", "The object with the provided " + 
342
                "identifier was not found.");
343
            
344
        } catch (SQLException ee) {
345
            throw new ServiceFailure("1310", "The object with the provided " + 
346
                    "identifier "+pid.getValue()+" can't be identified since - "+ee.getMessage());
347
        }
348
        
349
        // set the originating node
350
        NodeReference originMemberNode = this.getCapabilities().getIdentifier();
351
        sysmeta.setOriginMemberNode(originMemberNode);
352
        
353
        // set the submitter to match the certificate
354
        sysmeta.setSubmitter(subject);
355
        // set the dates
356
        Date now = Calendar.getInstance().getTime();
357
        sysmeta.setDateSysMetadataModified(now);
358
        sysmeta.setDateUploaded(now);
359
        
360
        // make sure serial version is set to something
361
        BigInteger serialVersion = sysmeta.getSerialVersion();
362
        if (serialVersion == null) {
363
        	sysmeta.setSerialVersion(BigInteger.ZERO);
364
        }
365

    
366
        // does the subject have WRITE ( == update) priveleges on the pid?
367
        allowed = isAuthorized(session, pid, Permission.WRITE);
368

    
369
        if (allowed) {
370
        	
371
        	// check quality of SM
372
        	if (sysmeta.getObsoletedBy() != null) {
373
        		throw new InvalidSystemMetadata("1300", "Cannot include obsoletedBy when updating object");
374
        	}
375
        	if (sysmeta.getObsoletes() != null && !sysmeta.getObsoletes().getValue().equals(pid.getValue())) {
376
        		throw new InvalidSystemMetadata("1300", "The identifier provided in obsoletes does not match old Identifier");
377
        	}
378

    
379
            // get the existing system metadata for the object
380
            SystemMetadata existingSysMeta = getSystemMetadata(session, pid);
381

    
382
            // check for previous update
383
            // see: https://redmine.dataone.org/issues/3336
384
            Identifier existingObsoletedBy = existingSysMeta.getObsoletedBy();
385
            if (existingObsoletedBy != null) {
386
            	throw new InvalidRequest("1202", 
387
            			"The previous identifier has already been made obsolete by: " + existingObsoletedBy.getValue());
388
            }
389
            //check the sid in the system metadata. If it exists, it should be non-exist or match the old sid in the previous system metadata.
390
            Identifier sidInSys = sysmeta.getSeriesId();
391
            if(sidInSys != null) {
392
                if (!isValidIdentifier(sidInSys)) {
393
                    throw new InvalidSystemMetadata("1300", "The provided series id in the system metadata is invalid.");
394
                }
395
                Identifier previousSid = existingSysMeta.getSeriesId();
396
                if(previousSid != null) {
397
                    // there is a previous sid, if the new sid doesn't match it, the new sid should be non-existing.
398
                    if(!sidInSys.getValue().equals(previousSid)) {
399
                        try {
400
                            idExists = IdentifierManager.getInstance().identifierExists(sidInSys.getValue());
401
                        } catch (SQLException e) {
402
                            throw new ServiceFailure("1310", 
403
                                                    "The requested identifier " + sidInSys.getValue() +
404
                                                    " couldn't be determined if it is unique since : "+e.getMessage());
405
                        }
406
                        if(idExists) {
407
                            throw new InvalidSystemMetadata("1300", "The series id "+sidInSys.getValue()+" in the system metadata doesn't match the previous series id "
408
                                                            +previousSid.getValue()+", so it should NOT exist. However, it was used by another object.");
409
                        }
410
                    }
411
                } else {
412
                    // there is no previous sid, the new sid should be non-existing.
413
                    try {
414
                        idExists = IdentifierManager.getInstance().identifierExists(sidInSys.getValue());
415
                    } catch (SQLException e) {
416
                        throw new ServiceFailure("1310", 
417
                                                "The requested identifier " + sidInSys.getValue() +
418
                                                " couldn't be determined if it is unique since : "+e.getMessage());
419
                    }
420
                    if(idExists) {
421
                        throw new InvalidSystemMetadata("1300", "The series id "+sidInSys.getValue()+" in the system metadata should NOT exist since the previous series id is null."
422
                                                        +"However, it was used by another object.");
423
                    }
424
                }
425
                
426
            }
427

    
428
            isScienceMetadata = isScienceMetadata(sysmeta);
429

    
430
            // do we have XML metadata or a data object?
431
            if (isScienceMetadata) {
432

    
433
                // update the science metadata XML document
434
                // TODO: handle non-XML metadata/data documents (like netCDF)
435
                // TODO: don't put objects into memory using stream to string
436
                //String objectAsXML = "";
437
                try {
438
                    //objectAsXML = IOUtils.toString(object, "UTF-8");
439
                    // give the old pid so we can calculate the new local id 
440
                    localId = insertOrUpdateDocument(object, "UTF-8", pid, session, "update");
441
                    // register the newPid and the generated localId
442
                    if (newPid != null) {
443
                        IdentifierManager.getInstance().createMapping(newPid.getValue(), localId);
444

    
445
                    }
446

    
447
                } catch (IOException e) {
448
                    String msg = "The Node is unable to create the object. " + "There was a problem converting the object to XML";
449
                    logMetacat.info(msg);
450
                    throw new ServiceFailure("1310", msg + ": " + e.getMessage());
451

    
452
                }
453

    
454
            } else {
455

    
456
                // update the data object
457
                localId = insertDataObject(object, newPid, session);
458

    
459
            }
460
            
461
            // add the newPid to the obsoletedBy list for the existing sysmeta
462
            existingSysMeta.setObsoletedBy(newPid);
463

    
464
            // then update the existing system metadata
465
            updateSystemMetadata(existingSysMeta);
466

    
467
            // prep the new system metadata, add pid to the affected lists
468
            sysmeta.setObsoletes(pid);
469
            //sysmeta.addDerivedFrom(pid);
470

    
471
            // and insert the new system metadata
472
            insertSystemMetadata(sysmeta);
473

    
474
            // log the update event
475
            EventLog.getInstance().log(request.getRemoteAddr(), request.getHeader("User-Agent"), subject.getValue(), localId, Event.UPDATE.toString());
476
            
477
            // attempt to register the identifier - it checks if it is a doi
478
            try {
479
    			DOIService.getInstance().registerDOI(sysmeta);
480
    		} catch (Exception e) {
481
                throw new ServiceFailure("1190", "Could not register DOI: " + e.getMessage());
482
    		}
483

    
484
        } else {
485
            throw new NotAuthorized("1200", "The provided identity does not have " + "permission to UPDATE the object identified by " + pid.getValue()
486
                    + " on the Member Node.");
487
        }
488

    
489
        return newPid;
490
    }
491

    
492
    public Identifier create(Session session, Identifier pid, InputStream object, SystemMetadata sysmeta) throws InvalidToken, ServiceFailure, NotAuthorized,
493
            IdentifierNotUnique, UnsupportedType, InsufficientResources, InvalidSystemMetadata, NotImplemented, InvalidRequest {
494

    
495
        // check for null session
496
        if (session == null) {
497
          throw new InvalidToken("1110", "Session is required to WRITE to the Node.");
498
        }
499
        // set the submitter to match the certificate
500
        sysmeta.setSubmitter(session.getSubject());
501
        // set the originating node
502
        NodeReference originMemberNode = this.getCapabilities().getIdentifier();
503
        sysmeta.setOriginMemberNode(originMemberNode);
504
        sysmeta.setArchived(false);
505

    
506
        // set the dates
507
        Date now = Calendar.getInstance().getTime();
508
        sysmeta.setDateSysMetadataModified(now);
509
        sysmeta.setDateUploaded(now);
510
        
511
        // set the serial version
512
        sysmeta.setSerialVersion(BigInteger.ZERO);
513

    
514
        // check that we are not attempting to subvert versioning
515
        if (sysmeta.getObsoletes() != null && sysmeta.getObsoletes().getValue() != null) {
516
            throw new InvalidSystemMetadata("1180", 
517
              "The supplied system metadata is invalid. " +
518
              "The obsoletes field cannot have a value when creating entries.");
519
        }
520
        
521
        if (sysmeta.getObsoletedBy() != null && sysmeta.getObsoletedBy().getValue() != null) {
522
            throw new InvalidSystemMetadata("1180", 
523
              "The supplied system metadata is invalid. " +
524
              "The obsoletedBy field cannot have a value when creating entries.");
525
        }
526

    
527
        // call the shared impl
528
        Identifier resultPid = super.create(session, pid, object, sysmeta);
529
        
530
        // attempt to register the identifier - it checks if it is a doi
531
        try {
532
			DOIService.getInstance().registerDOI(sysmeta);
533
		} catch (Exception e) {
534
			ServiceFailure sf = new ServiceFailure("1190", "Could not register DOI: " + e.getMessage());
535
			sf.initCause(e);
536
            throw sf;
537
		}
538
        
539
        // return 
540
		return resultPid ;
541
    }
542

    
543
    /**
544
     * Called by a Coordinating Node to request that the Member Node create a 
545
     * copy of the specified object by retrieving it from another Member 
546
     * Node and storing it locally so that it can be made accessible to 
547
     * the DataONE system.
548
     * 
549
     * @param session - the Session object containing the credentials for the Subject
550
     * @param sysmeta - Copy of the CN held system metadata for the object
551
     * @param sourceNode - A reference to node from which the content should be 
552
     *                     retrieved. The reference should be resolved by 
553
     *                     checking the CN node registry.
554
     * 
555
     * @return true if the replication succeeds
556
     * 
557
     * @throws ServiceFailure
558
     * @throws NotAuthorized
559
     * @throws NotImplemented
560
     * @throws UnsupportedType
561
     * @throws InsufficientResources
562
     * @throws InvalidRequest
563
     */
564
    @Override
565
    public boolean replicate(Session session, SystemMetadata sysmeta,
566
            NodeReference sourceNode) throws NotImplemented, ServiceFailure,
567
            NotAuthorized, InvalidRequest, InsufficientResources,
568
            UnsupportedType {
569

    
570
        if (session != null && sysmeta != null && sourceNode != null) {
571
            logMetacat.info("MNodeService.replicate() called with parameters: \n" +
572
                            "\tSession.Subject      = "                           +
573
                            session.getSubject().getValue() + "\n"                +
574
                            "\tidentifier           = "                           + 
575
                            sysmeta.getIdentifier().getValue()                    +
576
                            "\n" + "\tSource NodeReference ="                     +
577
                            sourceNode.getValue());
578
        }
579
        boolean result = false;
580
        String nodeIdStr = null;
581
        NodeReference nodeId = null;
582

    
583
        // get the referenced object
584
        Identifier pid = sysmeta.getIdentifier();
585

    
586
        // get from the membernode
587
        // TODO: switch credentials for the server retrieval?
588
        this.mn = D1Client.getMN(sourceNode);
589
        this.cn = D1Client.getCN();
590
        InputStream object = null;
591
        Session thisNodeSession = null;
592
        SystemMetadata localSystemMetadata = null;
593
        BaseException failure = null;
594
        String localId = null;
595
        
596
        // TODO: check credentials
597
        // cannot be called by public
598
        if (session == null || session.getSubject() == null) {
599
            String msg = "No session was provided to replicate identifier " +
600
            sysmeta.getIdentifier().getValue();
601
            logMetacat.error(msg);
602
            throw new NotAuthorized("2152", msg);
603
            
604
        }
605

    
606

    
607
        // get the local node id
608
        try {
609
            nodeIdStr = PropertyService.getProperty("dataone.nodeId");
610
            nodeId = new NodeReference();
611
            nodeId.setValue(nodeIdStr);
612

    
613
        } catch (PropertyNotFoundException e1) {
614
            String msg = "Couldn't get dataone.nodeId property: " + e1.getMessage();
615
            failure = new ServiceFailure("2151", msg);
616
            //setReplicationStatus(thisNodeSession, pid, nodeId, ReplicationStatus.FAILED, failure);
617
            logMetacat.error(msg);
618
            //return true;
619
            throw new ServiceFailure("2151", msg);
620

    
621
        }
622
        
623

    
624
        try {
625
            // do we already have a replica?
626
            try {
627
                localId = IdentifierManager.getInstance().getLocalId(pid.getValue());
628
                // if we have a local id, get the local object
629
                try {
630
                    object = MetacatHandler.read(localId);
631
                } catch (Exception e) {
632
                	// NOTE: we may already know about this ID because it could be a data file described by a metadata file
633
                	// https://redmine.dataone.org/issues/2572
634
                	// TODO: fix this so that we don't prevent ourselves from getting replicas
635
                	
636
                    // let the CN know that the replication failed
637
                	logMetacat.warn("Object content not found on this node despite having localId: " + localId);
638
                	String msg = "Can't read the object bytes properly, replica is invalid.";
639
                    ServiceFailure serviceFailure = new ServiceFailure("2151", msg);
640
                    setReplicationStatus(thisNodeSession, pid, nodeId, ReplicationStatus.FAILED, serviceFailure);
641
                    logMetacat.warn(msg);
642
                    throw serviceFailure;
643
                    
644
                }
645

    
646
            } catch (McdbDocNotFoundException e) {
647
                logMetacat.info("No replica found. Continuing.");
648
                
649
            } catch (SQLException ee) {
650
                throw new ServiceFailure("2151", "Couldn't identify the local id of the object with the specified identifier "
651
                                        +pid.getValue()+" since - "+ee.getMessage());
652
            }
653
            
654
            // no local replica, get a replica
655
            if ( object == null ) {
656
                // session should be null to use the default certificate
657
                // location set in the Certificate manager
658
                object = mn.getReplica(thisNodeSession, pid);
659
                logMetacat.info("MNodeService.getReplica() called for identifier "
660
                                + pid.getValue());
661

    
662
            }
663

    
664
        } catch (InvalidToken e) {            
665
            String msg = "Could not retrieve object to replicate (InvalidToken): "+ e.getMessage();
666
            failure = new ServiceFailure("2151", msg);
667
            setReplicationStatus(thisNodeSession, pid, nodeId, ReplicationStatus.FAILED, failure);
668
            logMetacat.error(msg);
669
            throw new ServiceFailure("2151", msg);
670

    
671
        } catch (NotFound e) {
672
            String msg = "Could not retrieve object to replicate (NotFound): "+ e.getMessage();
673
            failure = new ServiceFailure("2151", msg);
674
            setReplicationStatus(thisNodeSession, pid, nodeId, ReplicationStatus.FAILED, failure);
675
            logMetacat.error(msg);
676
            throw new ServiceFailure("2151", msg);
677

    
678
        } catch (NotAuthorized e) {
679
            String msg = "Could not retrieve object to replicate (NotAuthorized): "+ e.getMessage();
680
            failure = new ServiceFailure("2151", msg);
681
            setReplicationStatus(thisNodeSession, pid, nodeId, ReplicationStatus.FAILED, failure);
682
            logMetacat.error(msg);
683
            throw new ServiceFailure("2151", msg);
684
        } catch (NotImplemented e) {
685
            String msg = "Could not retrieve object to replicate (mn.getReplica NotImplemented): "+ e.getMessage();
686
            failure = new ServiceFailure("2151", msg);
687
            setReplicationStatus(thisNodeSession, pid, nodeId, ReplicationStatus.FAILED, failure);
688
            logMetacat.error(msg);
689
            throw new ServiceFailure("2151", msg);
690
        } catch (ServiceFailure e) {
691
            String msg = "Could not retrieve object to replicate (ServiceFailure): "+ e.getMessage();
692
            failure = new ServiceFailure("2151", msg);
693
            setReplicationStatus(thisNodeSession, pid, nodeId, ReplicationStatus.FAILED, failure);
694
            logMetacat.error(msg);
695
            throw new ServiceFailure("2151", msg);
696
        } catch (InsufficientResources e) {
697
            String msg = "Could not retrieve object to replicate (InsufficientResources): "+ e.getMessage();
698
            failure = new ServiceFailure("2151", msg);
699
            setReplicationStatus(thisNodeSession, pid, nodeId, ReplicationStatus.FAILED, failure);
700
            logMetacat.error(msg);
701
            throw new ServiceFailure("2151", msg);
702
        }
703

    
704
        // verify checksum on the object, if supported
705
        if (object.markSupported()) {
706
            Checksum givenChecksum = sysmeta.getChecksum();
707
            Checksum computedChecksum = null;
708
            try {
709
                computedChecksum = ChecksumUtil.checksum(object, givenChecksum.getAlgorithm());
710
                object.reset();
711

    
712
            } catch (Exception e) {
713
                String msg = "Error computing checksum on replica: " + e.getMessage();
714
                logMetacat.error(msg);
715
                ServiceFailure sf = new ServiceFailure("2151", msg);
716
                sf.initCause(e);
717
                setReplicationStatus(thisNodeSession, pid, nodeId, ReplicationStatus.FAILED, sf);
718
                throw sf;
719
            }
720
            if (!givenChecksum.getValue().equals(computedChecksum.getValue())) {
721
                logMetacat.error("Given    checksum for " + pid.getValue() + 
722
                    "is " + givenChecksum.getValue());
723
                logMetacat.error("Computed checksum for " + pid.getValue() + 
724
                    "is " + computedChecksum.getValue());
725
                String msg = "Computed checksum does not match declared checksum";
726
                failure = new ServiceFailure("2151", msg);
727
                setReplicationStatus(thisNodeSession, pid, nodeId, ReplicationStatus.FAILED, failure);
728
                throw new ServiceFailure("2151", msg);
729
            }
730
        }
731

    
732
        // add it to local store
733
        Identifier retPid;
734
        try {
735
            // skip the MN.create -- this mutates the system metadata and we don't want it to
736
            if ( localId == null ) {
737
                // TODO: this will fail if we already "know" about the identifier
738
            	// FIXME: see https://redmine.dataone.org/issues/2572
739
                retPid = super.create(session, pid, object, sysmeta);
740
                result = (retPid.getValue().equals(pid.getValue()));
741
            }
742
            
743
        } catch (Exception e) {
744
            String msg = "Could not save object to local store (" + e.getClass().getName() + "): " + e.getMessage();
745
            failure = new ServiceFailure("2151", msg);
746
            setReplicationStatus(thisNodeSession, pid, nodeId, ReplicationStatus.FAILED, failure);
747
            logMetacat.error(msg);
748
            throw new ServiceFailure("2151", msg);
749
            
750
        }
751

    
752
        // finish by setting the replication status
753
        setReplicationStatus(thisNodeSession, pid, nodeId, ReplicationStatus.COMPLETED, null);
754
        return result;
755

    
756
    }
757

    
758
    /**
759
     * Return the object identified by the given object identifier
760
     * 
761
     * @param session - the Session object containing the credentials for the Subject
762
     * @param pid - the object identifier for the given object
763
     * 
764
     * @return inputStream - the input stream of the given object
765
     * 
766
     * @throws InvalidToken
767
     * @throws ServiceFailure
768
     * @throws NotAuthorized
769
     * @throws InvalidRequest
770
     * @throws NotImplemented
771
     */
772
    @Override
773
    public InputStream get(Session session, Identifier pid) 
774
    throws InvalidToken, ServiceFailure, NotAuthorized, NotFound, NotImplemented {
775

    
776
        return super.get(session, pid);
777

    
778
    }
779

    
780
    /**
781
     * Returns a Checksum for the specified object using an accepted hashing algorithm
782
     * 
783
     * @param session - the Session object containing the credentials for the Subject
784
     * @param pid - the object identifier for the given object
785
     * @param algorithm -  the name of an algorithm that will be used to compute 
786
     *                     a checksum of the bytes of the object
787
     * 
788
     * @return checksum - the checksum of the given object
789
     * 
790
     * @throws InvalidToken
791
     * @throws ServiceFailure
792
     * @throws NotAuthorized
793
     * @throws NotFound
794
     * @throws InvalidRequest
795
     * @throws NotImplemented
796
     */
797
    @Override
798
    public Checksum getChecksum(Session session, Identifier pid, String algorithm) 
799
        throws InvalidToken, ServiceFailure, NotAuthorized, NotFound,
800
        InvalidRequest, NotImplemented {
801

    
802
        Checksum checksum = null;
803
        String serviceFailure = "1410";
804
        String notFound = "1420";
805
        //Checkum only handles the pid, not sid
806
        checkV1SystemMetaPidExist(pid, serviceFailure, "The checksum for the object specified by "+pid.getValue()+" couldn't be returned ",  notFound, 
807
                "The object specified by "+pid.getValue()+" does not exist at this node.");
808
        InputStream inputStream = get(session, pid);
809

    
810
        try {
811
            checksum = ChecksumUtil.checksum(inputStream, algorithm);
812

    
813
        } catch (NoSuchAlgorithmException e) {
814
            throw new ServiceFailure("1410", "The checksum for the object specified by " + pid.getValue() + "could not be returned due to an internal error: "
815
                    + e.getMessage());
816
        } catch (IOException e) {
817
            throw new ServiceFailure("1410", "The checksum for the object specified by " + pid.getValue() + "could not be returned due to an internal error: "
818
                    + e.getMessage());
819
        }
820

    
821
        if (checksum == null) {
822
            throw new ServiceFailure("1410", "The checksum for the object specified by " + pid.getValue() + "could not be returned.");
823
        }
824

    
825
        return checksum;
826
    }
827

    
828
    /**
829
     * Return the system metadata for a given object
830
     * 
831
     * @param session - the Session object containing the credentials for the Subject
832
     * @param pid - the object identifier for the given object
833
     * 
834
     * @return inputStream - the input stream of the given system metadata object
835
     * 
836
     * @throws InvalidToken
837
     * @throws ServiceFailure
838
     * @throws NotAuthorized
839
     * @throws NotFound
840
     * @throws InvalidRequest
841
     * @throws NotImplemented
842
     */
843
    @Override
844
    public SystemMetadata getSystemMetadata(Session session, Identifier pid) 
845
        throws InvalidToken, ServiceFailure, NotAuthorized, NotFound,
846
        NotImplemented {
847

    
848
        return super.getSystemMetadata(session, pid);
849
    }
850

    
851
    /**
852
     * Retrieve the list of objects present on the MN that match the calling parameters
853
     * 
854
     * @param session - the Session object containing the credentials for the Subject
855
     * @param startTime - Specifies the beginning of the time range from which 
856
     *                    to return object (>=)
857
     * @param endTime - Specifies the beginning of the time range from which 
858
     *                  to return object (>=)
859
     * @param objectFormat - Restrict results to the specified object format
860
     * @param replicaStatus - Indicates if replicated objects should be returned in the list
861
     * @param start - The zero-based index of the first value, relative to the 
862
     *                first record of the resultset that matches the parameters.
863
     * @param count - The maximum number of entries that should be returned in 
864
     *                the response. The Member Node may return less entries 
865
     *                than specified in this value.
866
     * 
867
     * @return objectList - the list of objects matching the criteria
868
     * 
869
     * @throws InvalidToken
870
     * @throws ServiceFailure
871
     * @throws NotAuthorized
872
     * @throws InvalidRequest
873
     * @throws NotImplemented
874
     */
875
    @Override
876
    public ObjectList listObjects(Session session, Date startTime, Date endTime, ObjectFormatIdentifier objectFormatId, Identifier identifier, Boolean replicaStatus, Integer start,
877
            Integer count) throws NotAuthorized, InvalidRequest, NotImplemented, ServiceFailure, InvalidToken {
878

    
879
        ObjectList objectList = null;
880

    
881
        try {
882
        	// safeguard against large requests
883
            if (count == null || count > MAXIMUM_DB_RECORD_COUNT) {
884
            	count = MAXIMUM_DB_RECORD_COUNT;
885
            }
886
            objectList = IdentifierManager.getInstance().querySystemMetadata(startTime, endTime, objectFormatId, replicaStatus, start, count);
887
        } catch (Exception e) {
888
            throw new ServiceFailure("1580", "Error querying system metadata: " + e.getMessage());
889
        }
890

    
891
        return objectList;
892
    }
893

    
894
    /**
895
     * Return a description of the node's capabilities and services.
896
     * 
897
     * @return node - the technical capabilities of the Member Node
898
     * 
899
     * @throws ServiceFailure
900
     * @throws NotAuthorized
901
     * @throws InvalidRequest
902
     * @throws NotImplemented
903
     */
904
    @Override
905
    public Node getCapabilities() 
906
        throws NotImplemented, ServiceFailure {
907

    
908
        String nodeName = null;
909
        String nodeId = null;
910
        String subject = null;
911
        String contactSubject = null;
912
        String nodeDesc = null;
913
        String nodeTypeString = null;
914
        NodeType nodeType = null;
915
        String mnCoreServiceVersion = null;
916
        String mnReadServiceVersion = null;
917
        String mnAuthorizationServiceVersion = null;
918
        String mnStorageServiceVersion = null;
919
        String mnReplicationServiceVersion = null;
920

    
921
        boolean nodeSynchronize = false;
922
        boolean nodeReplicate = false;
923
        boolean mnCoreServiceAvailable = false;
924
        boolean mnReadServiceAvailable = false;
925
        boolean mnAuthorizationServiceAvailable = false;
926
        boolean mnStorageServiceAvailable = false;
927
        boolean mnReplicationServiceAvailable = false;
928

    
929
        try {
930
            // get the properties of the node based on configuration information
931
            nodeName = PropertyService.getProperty("dataone.nodeName");
932
            nodeId = PropertyService.getProperty("dataone.nodeId");
933
            subject = PropertyService.getProperty("dataone.subject");
934
            contactSubject = PropertyService.getProperty("dataone.contactSubject");
935
            nodeDesc = PropertyService.getProperty("dataone.nodeDescription");
936
            nodeTypeString = PropertyService.getProperty("dataone.nodeType");
937
            nodeType = NodeType.convert(nodeTypeString);
938
            nodeSynchronize = new Boolean(PropertyService.getProperty("dataone.nodeSynchronize")).booleanValue();
939
            nodeReplicate = new Boolean(PropertyService.getProperty("dataone.nodeReplicate")).booleanValue();
940

    
941
            mnCoreServiceVersion = PropertyService.getProperty("dataone.mnCore.serviceVersion");
942
            mnReadServiceVersion = PropertyService.getProperty("dataone.mnRead.serviceVersion");
943
            mnAuthorizationServiceVersion = PropertyService.getProperty("dataone.mnAuthorization.serviceVersion");
944
            mnStorageServiceVersion = PropertyService.getProperty("dataone.mnStorage.serviceVersion");
945
            mnReplicationServiceVersion = PropertyService.getProperty("dataone.mnReplication.serviceVersion");
946

    
947
            mnCoreServiceAvailable = new Boolean(PropertyService.getProperty("dataone.mnCore.serviceAvailable")).booleanValue();
948
            mnReadServiceAvailable = new Boolean(PropertyService.getProperty("dataone.mnRead.serviceAvailable")).booleanValue();
949
            mnAuthorizationServiceAvailable = new Boolean(PropertyService.getProperty("dataone.mnAuthorization.serviceAvailable")).booleanValue();
950
            mnStorageServiceAvailable = new Boolean(PropertyService.getProperty("dataone.mnStorage.serviceAvailable")).booleanValue();
951
            mnReplicationServiceAvailable = new Boolean(PropertyService.getProperty("dataone.mnReplication.serviceAvailable")).booleanValue();
952

    
953
            // Set the properties of the node based on configuration information and
954
            // calls to current status methods
955
            String serviceName = SystemUtil.getSecureContextURL() + "/" + PropertyService.getProperty("dataone.serviceName");
956
            Node node = new Node();
957
            node.setBaseURL(serviceName + "/" + nodeTypeString);
958
            node.setDescription(nodeDesc);
959

    
960
            // set the node's health information
961
            node.setState(NodeState.UP);
962
            
963
            // set the ping response to the current value
964
            Ping canPing = new Ping();
965
            canPing.setSuccess(false);
966
            try {
967
            	Date pingDate = ping();
968
                canPing.setSuccess(pingDate != null);
969
            } catch (BaseException e) {
970
                e.printStackTrace();
971
                // guess it can't be pinged
972
            }
973
            
974
            node.setPing(canPing);
975

    
976
            NodeReference identifier = new NodeReference();
977
            identifier.setValue(nodeId);
978
            node.setIdentifier(identifier);
979
            Subject s = new Subject();
980
            s.setValue(subject);
981
            node.addSubject(s);
982
            Subject contact = new Subject();
983
            contact.setValue(contactSubject);
984
            node.addContactSubject(contact);
985
            node.setName(nodeName);
986
            node.setReplicate(nodeReplicate);
987
            node.setSynchronize(nodeSynchronize);
988

    
989
            // services: MNAuthorization, MNCore, MNRead, MNReplication, MNStorage
990
            Services services = new Services();
991

    
992
            Service sMNCore = new Service();
993
            sMNCore.setName("MNCore");
994
            sMNCore.setVersion(mnCoreServiceVersion);
995
            sMNCore.setAvailable(mnCoreServiceAvailable);
996

    
997
            Service sMNRead = new Service();
998
            sMNRead.setName("MNRead");
999
            sMNRead.setVersion(mnReadServiceVersion);
1000
            sMNRead.setAvailable(mnReadServiceAvailable);
1001

    
1002
            Service sMNAuthorization = new Service();
1003
            sMNAuthorization.setName("MNAuthorization");
1004
            sMNAuthorization.setVersion(mnAuthorizationServiceVersion);
1005
            sMNAuthorization.setAvailable(mnAuthorizationServiceAvailable);
1006

    
1007
            Service sMNStorage = new Service();
1008
            sMNStorage.setName("MNStorage");
1009
            sMNStorage.setVersion(mnStorageServiceVersion);
1010
            sMNStorage.setAvailable(mnStorageServiceAvailable);
1011

    
1012
            Service sMNReplication = new Service();
1013
            sMNReplication.setName("MNReplication");
1014
            sMNReplication.setVersion(mnReplicationServiceVersion);
1015
            sMNReplication.setAvailable(mnReplicationServiceAvailable);
1016

    
1017
            services.addService(sMNRead);
1018
            services.addService(sMNCore);
1019
            services.addService(sMNAuthorization);
1020
            services.addService(sMNStorage);
1021
            services.addService(sMNReplication);
1022
            node.setServices(services);
1023

    
1024
            // Set the schedule for synchronization
1025
            Synchronization synchronization = new Synchronization();
1026
            Schedule schedule = new Schedule();
1027
            Date now = new Date();
1028
            schedule.setYear(PropertyService.getProperty("dataone.nodeSynchronization.schedule.year"));
1029
            schedule.setMon(PropertyService.getProperty("dataone.nodeSynchronization.schedule.mon"));
1030
            schedule.setMday(PropertyService.getProperty("dataone.nodeSynchronization.schedule.mday"));
1031
            schedule.setWday(PropertyService.getProperty("dataone.nodeSynchronization.schedule.wday"));
1032
            schedule.setHour(PropertyService.getProperty("dataone.nodeSynchronization.schedule.hour"));
1033
            schedule.setMin(PropertyService.getProperty("dataone.nodeSynchronization.schedule.min"));
1034
            schedule.setSec(PropertyService.getProperty("dataone.nodeSynchronization.schedule.sec"));
1035
            synchronization.setSchedule(schedule);
1036
            synchronization.setLastHarvested(now);
1037
            synchronization.setLastCompleteHarvest(now);
1038
            node.setSynchronization(synchronization);
1039

    
1040
            node.setType(nodeType);
1041
            return node;
1042

    
1043
        } catch (PropertyNotFoundException pnfe) {
1044
            String msg = "MNodeService.getCapabilities(): " + "property not found: " + pnfe.getMessage();
1045
            logMetacat.error(msg);
1046
            throw new ServiceFailure("2162", msg);
1047
        }
1048
    }
1049

    
1050
    
1051

    
1052
    /**
1053
     * A callback method used by a CN to indicate to a MN that it cannot 
1054
     * complete synchronization of the science metadata identified by pid.  Log
1055
     * the event in the metacat event log.
1056
     * 
1057
     * @param session
1058
     * @param syncFailed
1059
     * 
1060
     * @throws ServiceFailure
1061
     * @throws NotAuthorized
1062
     * @throws NotImplemented
1063
     */
1064
    @Override
1065
    public boolean synchronizationFailed(Session session, SynchronizationFailed syncFailed) 
1066
        throws NotImplemented, ServiceFailure, NotAuthorized {
1067

    
1068
        String localId;
1069
        Identifier pid;
1070
        if ( syncFailed.getPid() != null ) {
1071
            pid = new Identifier();
1072
            pid.setValue(syncFailed.getPid());
1073
            boolean allowed;
1074
            
1075
            //are we allowed? only CNs
1076
            try {
1077
                allowed = isAdminAuthorized(session);
1078
                if ( !allowed ){
1079
                    throw new NotAuthorized("2162", 
1080
                            "Not allowed to call synchronizationFailed() on this node.");
1081
                }
1082
            } catch (InvalidToken e) {
1083
                throw new NotAuthorized("2162", 
1084
                        "Not allowed to call synchronizationFailed() on this node.");
1085

    
1086
            }
1087
            
1088
        } else {
1089
            throw new ServiceFailure("2161", "The identifier cannot be null.");
1090

    
1091
        }
1092
        
1093
        try {
1094
            localId = IdentifierManager.getInstance().getLocalId(pid.getValue());
1095
        } catch (McdbDocNotFoundException e) {
1096
            throw new ServiceFailure("2161", "The identifier specified by " + 
1097
                    syncFailed.getPid() + " was not found on this node.");
1098

    
1099
        } catch (SQLException e) {
1100
            throw new ServiceFailure("2161", "Couldn't identify the local id of the identifier specified by " + 
1101
                    syncFailed.getPid() + " since "+e.getMessage());
1102
        }
1103
        // TODO: update the CN URL below when the CNRead.SynchronizationFailed
1104
        // method is changed to include the URL as a parameter
1105
        logMetacat.debug("Synchronization for the object identified by " + 
1106
                pid.getValue() + " failed from " + syncFailed.getNodeId() + 
1107
                " Logging the event to the Metacat EventLog as a 'syncFailed' event.");
1108
        // TODO: use the event type enum when the SYNCHRONIZATION_FAILED event is added
1109
        String principal = Constants.SUBJECT_PUBLIC;
1110
        if (session != null && session.getSubject() != null) {
1111
          principal = session.getSubject().getValue();
1112
        }
1113
        try {
1114
          EventLog.getInstance().log(request.getRemoteAddr(), request.getHeader("User-Agent"), principal, localId, "synchronization_failed");
1115
        } catch (Exception e) {
1116
            throw new ServiceFailure("2161", "Could not log the error for: " + pid.getValue());
1117
        }
1118
        //EventLog.getInstance().log("CN URL WILL GO HERE", 
1119
        //  session.getSubject().getValue(), localId, Event.SYNCHRONIZATION_FAILED);
1120
        return true;
1121

    
1122
    }
1123

    
1124
    /**
1125
     * Essentially a get() but with different logging behavior
1126
     */
1127
    @Override
1128
    public InputStream getReplica(Session session, Identifier pid) 
1129
        throws NotAuthorized, NotImplemented, ServiceFailure, InvalidToken {
1130

    
1131
        logMetacat.info("MNodeService.getReplica() called.");
1132

    
1133
        // cannot be called by public
1134
        if (session == null) {
1135
        	throw new InvalidToken("2183", "No session was provided.");
1136
        }
1137
        
1138
        logMetacat.info("MNodeService.getReplica() called with parameters: \n" +
1139
             "\tSession.Subject      = " + session.getSubject().getValue() + "\n" +
1140
             "\tIdentifier           = " + pid.getValue());
1141

    
1142
        InputStream inputStream = null; // bytes to be returned
1143
        handler = new MetacatHandler(new Timer());
1144
        boolean allowed = false;
1145
        String localId; // the metacat docid for the pid
1146

    
1147
        // get the local docid from Metacat
1148
        try {
1149
            localId = IdentifierManager.getInstance().getLocalId(pid.getValue());
1150
        } catch (McdbDocNotFoundException e) {
1151
            throw new ServiceFailure("2181", "The object specified by " + 
1152
                    pid.getValue() + " does not exist at this node.");
1153
            
1154
        } catch (SQLException e) {
1155
            throw new ServiceFailure("2181", "The local id of the object specified by " + 
1156
                    pid.getValue() + " couldn't be identified since "+e.getMessage());
1157
        }
1158

    
1159
        Subject targetNodeSubject = session.getSubject();
1160

    
1161
        // check for authorization to replicate, null session to act as this source MN
1162
        try {
1163
            allowed = D1Client.getCN().isNodeAuthorized(null, targetNodeSubject, pid);
1164
        } catch (InvalidToken e1) {
1165
            throw new ServiceFailure("2181", "Could not determine if node is authorized: " 
1166
                + e1.getMessage());
1167
            
1168
        } catch (NotFound e1) {
1169
            throw new ServiceFailure("2181", "Could not determine if node is authorized: " 
1170
                    + e1.getMessage());
1171

    
1172
        } catch (InvalidRequest e1) {
1173
            throw new ServiceFailure("2181", "Could not determine if node is authorized: " 
1174
                    + e1.getMessage());
1175

    
1176
        }
1177

    
1178
        logMetacat.info("Called D1Client.isNodeAuthorized(). Allowed = " + allowed +
1179
            " for identifier " + pid.getValue());
1180

    
1181
        // if the person is authorized, perform the read
1182
        if (allowed) {
1183
            try {
1184
                inputStream = MetacatHandler.read(localId);
1185
            } catch (Exception e) {
1186
                throw new ServiceFailure("1020", "The object specified by " + 
1187
                    pid.getValue() + "could not be returned due to error: " + e.getMessage());
1188
            }
1189
        }
1190

    
1191
        // if we fail to set the input stream
1192
        if (inputStream == null) {
1193
            throw new ServiceFailure("2181", "The object specified by " + 
1194
                pid.getValue() + "does not exist at this node.");
1195
        }
1196

    
1197
        // log the replica event
1198
        String principal = null;
1199
        if (session.getSubject() != null) {
1200
            principal = session.getSubject().getValue();
1201
        }
1202
        EventLog.getInstance().log(request.getRemoteAddr(), 
1203
            request.getHeader("User-Agent"), principal, localId, "replicate");
1204

    
1205
        return inputStream;
1206
    }
1207

    
1208
    /**
1209
     * A method to notify the Member Node that the authoritative copy of 
1210
     * system metadata on the Coordinating Nodes has changed.
1211
     * 
1212
     * @param session   Session information that contains the identity of the 
1213
     *                  calling user as retrieved from the X.509 certificate 
1214
     *                  which must be traceable to the CILogon service.
1215
     * @param serialVersion   The serialVersion of the system metadata
1216
     * @param dateSysMetaLastModified  The time stamp for when the system metadata was changed
1217
     * @throws NotImplemented
1218
     * @throws ServiceFailure
1219
     * @throws NotAuthorized
1220
     * @throws InvalidRequest
1221
     * @throws InvalidToken
1222
     */
1223
    public boolean systemMetadataChanged(Session session, Identifier pid,
1224
        long serialVersion, Date dateSysMetaLastModified) 
1225
        throws NotImplemented, ServiceFailure, NotAuthorized, InvalidRequest,
1226
        InvalidToken {
1227
        
1228
        // cannot be called by public
1229
        if (session == null) {
1230
        	throw new InvalidToken("2183", "No session was provided.");
1231
        }
1232

    
1233
        SystemMetadata currentLocalSysMeta = null;
1234
        SystemMetadata newSysMeta = null;
1235
        CNode cn = D1Client.getCN();
1236
        NodeList nodeList = null;
1237
        Subject callingSubject = null;
1238
        boolean allowed = false;
1239
        
1240
        // are we allowed to call this?
1241
        callingSubject = session.getSubject();
1242
        nodeList = cn.listNodes();
1243
        
1244
        for(Node node : nodeList.getNodeList()) {
1245
            // must be a CN
1246
            if ( node.getType().equals(NodeType.CN)) {
1247
               List<Subject> subjectList = node.getSubjectList();
1248
               // the calling subject must be in the subject list
1249
               if ( subjectList.contains(callingSubject)) {
1250
                   allowed = true;
1251
                   
1252
               }
1253
               
1254
            }
1255
        }
1256
        
1257
        if (!allowed ) {
1258
            String msg = "The subject identified by " + callingSubject.getValue() +
1259
              " is not authorized to call this service.";
1260
            throw new NotAuthorized("1331", msg);
1261
            
1262
        }
1263
        
1264
        // compare what we have locally to what is sent in the change notification
1265
        try {
1266
            currentLocalSysMeta = HazelcastService.getInstance().getSystemMetadataMap().get(pid);
1267
             
1268
        } catch (RuntimeException e) {
1269
            String msg = "SystemMetadata for pid " + pid.getValue() +
1270
              " couldn't be updated because it couldn't be found locally: " +
1271
              e.getMessage();
1272
            logMetacat.error(msg);
1273
            ServiceFailure sf = new ServiceFailure("1333", msg);
1274
            sf.initCause(e);
1275
            throw sf; 
1276
        }
1277
        
1278
        if (currentLocalSysMeta.getSerialVersion().longValue() < serialVersion ) {
1279
            try {
1280
                newSysMeta = cn.getSystemMetadata(null, pid);
1281
            } catch (NotFound e) {
1282
                // huh? you just said you had it
1283
            	String msg = "On updating the local copy of system metadata " + 
1284
                "for pid " + pid.getValue() +", the CN reports it is not found." +
1285
                " The error message was: " + e.getMessage();
1286
                logMetacat.error(msg);
1287
                ServiceFailure sf = new ServiceFailure("1333", msg);
1288
                sf.initCause(e);
1289
                throw sf;
1290
            }
1291
            
1292
            // update the local copy of system metadata for the pid
1293
            try {
1294
                HazelcastService.getInstance().getSystemMetadataMap().put(newSysMeta.getIdentifier(), newSysMeta);
1295
                logMetacat.info("Updated local copy of system metadata for pid " +
1296
                    pid.getValue() + " after change notification from the CN.");
1297
                
1298
                // TODO: consider inspecting the change for archive
1299
                // see: https://projects.ecoinformatics.org/ecoinfo/issues/6417
1300
//                if (newSysMeta.getArchived() != null && newSysMeta.getArchived().booleanValue()) {
1301
//                	try {
1302
//						this.archive(session, newSysMeta.getIdentifier());
1303
//					} catch (NotFound e) {
1304
//						// do we care? nothing to do about it now
1305
//						logMetacat.error(e.getMessage(), e);
1306
//					}
1307
//                }
1308
                
1309
            } catch (RuntimeException e) {
1310
                String msg = "SystemMetadata for pid " + pid.getValue() +
1311
                  " couldn't be updated: " +
1312
                  e.getMessage();
1313
                logMetacat.error(msg);
1314
                ServiceFailure sf = new ServiceFailure("1333", msg);
1315
                sf.initCause(e);
1316
                throw sf;
1317
            }
1318
            
1319
            // submit for indexing
1320
            try {
1321
				MetacatSolrIndex.getInstance().submit(newSysMeta.getIdentifier(), newSysMeta, null, true);
1322
			} catch (Exception e) {
1323
                logMetacat.error("Could not submit changed systemMetadata for indexing, pid: " + newSysMeta.getIdentifier().getValue(), e);
1324
			}
1325
        }
1326
        
1327
        return true;
1328
        
1329
    }
1330
    
1331
    /*
1332
     * Set the replication status for the object on the Coordinating Node
1333
     * 
1334
     * @param session - the session for the this target node
1335
     * @param pid - the identifier of the object being updated
1336
     * @param nodeId - the identifier of this target node
1337
     * @param status - the replication status to set
1338
     * @param failure - the exception to include, if any
1339
     */
1340
    private void setReplicationStatus(Session session, Identifier pid, 
1341
        NodeReference nodeId, ReplicationStatus status, BaseException failure) 
1342
        throws ServiceFailure, NotImplemented, NotAuthorized, 
1343
        InvalidRequest {
1344
        
1345
        // call the CN as the MN to set the replication status
1346
        try {
1347
            this.cn = D1Client.getCN();
1348
            this.cn.setReplicationStatus(session, pid, nodeId,
1349
                    status, failure);
1350
            
1351
        } catch (InvalidToken e) {
1352
        	String msg = "Could not set the replication status for " + pid.getValue() + " on the CN (InvalidToken): " + e.getMessage();
1353
            logMetacat.error(msg);
1354
        	throw new ServiceFailure("2151",
1355
                    msg);
1356
            
1357
        } catch (NotFound e) {
1358
        	String msg = "Could not set the replication status for " + pid.getValue() + " on the CN (NotFound): " + e.getMessage();
1359
            logMetacat.error(msg);
1360
        	throw new ServiceFailure("2151",
1361
                    msg);
1362
            
1363
        }
1364
    }
1365
    
1366
    private SystemMetadata makePublicIfNot(SystemMetadata sysmeta, Identifier pid) throws ServiceFailure, InvalidToken, NotFound, NotImplemented, InvalidRequest {
1367
    	// check if it is publicly readable
1368
		boolean isPublic = false;
1369
		Subject publicSubject = new Subject();
1370
		publicSubject.setValue(Constants.SUBJECT_PUBLIC);
1371
		Session publicSession = new Session();
1372
		publicSession.setSubject(publicSubject);
1373
		AccessRule publicRule = new AccessRule();
1374
		publicRule.addPermission(Permission.READ);
1375
		publicRule.addSubject(publicSubject);
1376
		
1377
		// see if we need to add the rule
1378
		try {
1379
			isPublic = this.isAuthorized(publicSession, pid, Permission.READ);
1380
		} catch (NotAuthorized na) {
1381
			// well, certainly not authorized for public read!
1382
		}
1383
		if (!isPublic) {
1384
			sysmeta.getAccessPolicy().addAllow(publicRule);
1385
		}
1386
		
1387
		return sysmeta;
1388
    }
1389

    
1390
	@Override
1391
	public Identifier generateIdentifier(Session session, String scheme, String fragment)
1392
			throws InvalidToken, ServiceFailure, NotAuthorized, NotImplemented,
1393
			InvalidRequest {
1394
		
1395
		// check for null session
1396
        if (session == null) {
1397
          throw new InvalidToken("2190", "Session is required to generate an Identifier at this Node.");
1398
        }
1399
		
1400
		Identifier identifier = new Identifier();
1401
		
1402
		// handle different schemes
1403
		if (scheme.equalsIgnoreCase(UUID_SCHEME)) {
1404
			// UUID
1405
			UUID uuid = UUID.randomUUID();
1406
            identifier.setValue(UUID_PREFIX + uuid.toString());
1407
		} else if (scheme.equalsIgnoreCase(DOI_SCHEME)) {
1408
			// generate a DOI
1409
			try {
1410
				identifier = DOIService.getInstance().generateDOI();
1411
			} catch (EZIDException e) {
1412
				ServiceFailure sf = new ServiceFailure("2191", "Could not generate DOI: " + e.getMessage());
1413
				sf.initCause(e);
1414
				throw sf;
1415
			}
1416
		} else {
1417
			// default if we don't know the scheme
1418
			if (fragment != null) {
1419
				// for now, just autogen with fragment
1420
				String autogenId = DocumentUtil.generateDocumentId(fragment, 0);
1421
				identifier.setValue(autogenId);			
1422
			} else {
1423
				// autogen with no fragment
1424
				String autogenId = DocumentUtil.generateDocumentId(0);
1425
				identifier.setValue(autogenId);
1426
			}
1427
		}
1428
		
1429
		// TODO: reserve the identifier with the CN. We can only do this when
1430
		// 1) the MN is part of a CN cluster
1431
		// 2) the request is from an authenticated user
1432
		
1433
		return identifier;
1434
	}
1435

    
1436
	
1437

    
1438
	@Override
1439
	public QueryEngineDescription getQueryEngineDescription(Session session, String engine)
1440
			throws InvalidToken, ServiceFailure, NotAuthorized, NotImplemented,
1441
			NotFound {
1442
	    if(engine != null && engine.equals(EnabledQueryEngines.PATHQUERYENGINE)) {
1443
	        if(!EnabledQueryEngines.getInstance().isEnabled(EnabledQueryEngines.PATHQUERYENGINE)) {
1444
                throw new NotImplemented("0000", "MNodeService.query - the query engine "+engine +" hasn't been implemented or has been disabled.");
1445
            }
1446
	        QueryEngineDescription qed = new QueryEngineDescription();
1447
	        qed.setName(EnabledQueryEngines.PATHQUERYENGINE);
1448
	        qed.setQueryEngineVersion("1.0");
1449
	        qed.addAdditionalInfo("This is the traditional structured query for Metacat");
1450
	        Vector<String> pathsForIndexing = null;
1451
	        try {
1452
	            pathsForIndexing = SystemUtil.getPathsForIndexing();
1453
	        } catch (MetacatUtilException e) {
1454
	            logMetacat.warn("Could not get index paths", e);
1455
	        }
1456
	        for (String fieldName: pathsForIndexing) {
1457
	            QueryField field = new QueryField();
1458
	            field.addDescription("Indexed field for path '" + fieldName + "'");
1459
	            field.setName(fieldName);
1460
	            field.setReturnable(true);
1461
	            field.setSearchable(true);
1462
	            field.setSortable(false);
1463
	            // TODO: determine type and multivaluedness
1464
	            field.setType(String.class.getName());
1465
	            //field.setMultivalued(true);
1466
	            qed.addQueryField(field);
1467
	        }
1468
	        return qed;
1469
	    } else if (engine != null && engine.equals(EnabledQueryEngines.SOLRENGINE)) {
1470
	        if(!EnabledQueryEngines.getInstance().isEnabled(EnabledQueryEngines.SOLRENGINE)) {
1471
                throw new NotImplemented("0000", "MNodeService.getQueryEngineDescription - the query engine "+engine +" hasn't been implemented or has been disabled.");
1472
            }
1473
	        try {
1474
	            QueryEngineDescription qed = MetacatSolrEngineDescriptionHandler.getInstance().getQueryEngineDescritpion();
1475
	            return qed;
1476
	        } catch (Exception e) {
1477
	            e.printStackTrace();
1478
	            throw new ServiceFailure("Solr server error", e.getMessage());
1479
	        }
1480
	    } else {
1481
	        throw new NotFound("404", "The Metacat member node can't find the query engine - "+engine);
1482
	    }
1483
		
1484
	}
1485

    
1486
	@Override
1487
	public QueryEngineList listQueryEngines(Session session) throws InvalidToken,
1488
			ServiceFailure, NotAuthorized, NotImplemented {
1489
		QueryEngineList qel = new QueryEngineList();
1490
		//qel.addQueryEngine(EnabledQueryEngines.PATHQUERYENGINE);
1491
		//qel.addQueryEngine(EnabledQueryEngines.SOLRENGINE);
1492
		List<String> enables = EnabledQueryEngines.getInstance().getEnabled();
1493
		for(String name : enables) {
1494
		    qel.addQueryEngine(name);
1495
		}
1496
		return qel;
1497
	}
1498

    
1499
	@Override
1500
	public InputStream query(Session session, String engine, String query) throws InvalidToken,
1501
			ServiceFailure, NotAuthorized, InvalidRequest, NotImplemented,
1502
			NotFound {
1503
	    String user = Constants.SUBJECT_PUBLIC;
1504
        String[] groups= null;
1505
        Set<Subject> subjects = null;
1506
        if (session != null) {
1507
            user = session.getSubject().getValue();
1508
            subjects = AuthUtils.authorizedClientSubjects(session);
1509
            if (subjects != null) {
1510
                List<String> groupList = new ArrayList<String>();
1511
                for (Subject subject: subjects) {
1512
                    groupList.add(subject.getValue());
1513
                }
1514
                groups = groupList.toArray(new String[0]);
1515
            }
1516
        } else {
1517
            //add the public user subject to the set 
1518
            Subject subject = new Subject();
1519
            subject.setValue(Constants.SUBJECT_PUBLIC);
1520
            subjects = new HashSet<Subject>();
1521
            subjects.add(subject);
1522
        }
1523
        //System.out.println("====== user is "+user);
1524
        //System.out.println("====== groups are "+groups);
1525
		if (engine != null && engine.equals(EnabledQueryEngines.PATHQUERYENGINE)) {
1526
		    if(!EnabledQueryEngines.getInstance().isEnabled(EnabledQueryEngines.PATHQUERYENGINE)) {
1527
                throw new NotImplemented("0000", "MNodeService.query - the query engine "+engine +" hasn't been implemented or has been disabled.");
1528
            }
1529
			try {
1530
				DBQuery queryobj = new DBQuery();
1531
				
1532
				String results = queryobj.performPathquery(query, user, groups);
1533
				ContentTypeByteArrayInputStream ctbais = new ContentTypeByteArrayInputStream(results.getBytes(MetaCatServlet.DEFAULT_ENCODING));
1534
				ctbais.setContentType("text/xml");
1535
				return ctbais;
1536

    
1537
			} catch (Exception e) {
1538
				throw new ServiceFailure("Pathquery error", e.getMessage());
1539
			}
1540
			
1541
		} else if (engine != null && engine.equals(EnabledQueryEngines.SOLRENGINE)) {
1542
		    if(!EnabledQueryEngines.getInstance().isEnabled(EnabledQueryEngines.SOLRENGINE)) {
1543
		        throw new NotImplemented("0000", "MNodeService.query - the query engine "+engine +" hasn't been implemented or has been disabled.");
1544
		    }
1545
		    logMetacat.info("The query is ==================================== \n"+query);
1546
		    try {
1547
		        
1548
                return MetacatSolrIndex.getInstance().query(query, subjects);
1549
            } catch (Exception e) {
1550
                // TODO Auto-generated catch block
1551
                throw new ServiceFailure("Solr server error", e.getMessage());
1552
            } 
1553
		}
1554
		return null;
1555
	}
1556
	
1557
	/**
1558
	 * Given an existing Science Metadata PID, this method mints a DOI
1559
	 * and updates the original object "publishing" the update with the DOI.
1560
	 * This includes updating the ORE map that describes the Science Metadata+data.
1561
	 * TODO: ensure all referenced objects allow public read
1562
	 * 
1563
	 * @see https://projects.ecoinformatics.org/ecoinfo/issues/6014
1564
	 * 
1565
	 * @param originalIdentifier
1566
	 * @param request
1567
	 * @throws InvalidRequest 
1568
	 * @throws NotImplemented 
1569
	 * @throws NotAuthorized 
1570
	 * @throws ServiceFailure 
1571
	 * @throws InvalidToken 
1572
	 * @throws NotFound
1573
	 * @throws InvalidSystemMetadata 
1574
	 * @throws InsufficientResources 
1575
	 * @throws UnsupportedType 
1576
	 * @throws IdentifierNotUnique 
1577
	 */
1578
	public Identifier publish(Session session, Identifier originalIdentifier) throws InvalidToken, ServiceFailure, NotAuthorized, NotImplemented, InvalidRequest, NotFound, IdentifierNotUnique, UnsupportedType, InsufficientResources, InvalidSystemMetadata {
1579
		
1580
		
1581
		// get the original SM
1582
		SystemMetadata originalSystemMetadata = this.getSystemMetadata(session, originalIdentifier);
1583

    
1584
		// make copy of it using the marshaller to ensure DEEP copy
1585
		SystemMetadata sysmeta = null;
1586
		try {
1587
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
1588
			TypeMarshaller.marshalTypeToOutputStream(originalSystemMetadata, baos);
1589
			sysmeta = TypeMarshaller.unmarshalTypeFromStream(SystemMetadata.class, new ByteArrayInputStream(baos.toByteArray()));
1590
		} catch (Exception e) {
1591
			// report as service failure
1592
			ServiceFailure sf = new ServiceFailure("1030", e.getMessage());
1593
			sf.initCause(e);
1594
			throw sf;
1595
		}
1596

    
1597
		// mint a DOI for the new revision
1598
		Identifier newIdentifier = this.generateIdentifier(session, MNodeService.DOI_SCHEME, null);
1599
				
1600
		// set new metadata values
1601
		sysmeta.setIdentifier(newIdentifier);
1602
		sysmeta.setObsoletes(originalIdentifier);
1603
		sysmeta.setObsoletedBy(null);
1604
		
1605
		// ensure it is publicly readable
1606
		sysmeta = makePublicIfNot(sysmeta, originalIdentifier);
1607
		
1608
		// get the bytes
1609
		InputStream inputStream = this.get(session, originalIdentifier);
1610
		
1611
		// update the object
1612
		this.update(session, originalIdentifier, inputStream, newIdentifier, sysmeta);
1613
		
1614
		// update ORE that references the scimeta
1615
		// first try the naive method, then check the SOLR index
1616
		try {
1617
			String localId = IdentifierManager.getInstance().getLocalId(originalIdentifier.getValue());
1618
			
1619
			Identifier potentialOreIdentifier = new Identifier();
1620
			potentialOreIdentifier.setValue(SystemMetadataFactory.RESOURCE_MAP_PREFIX + localId);
1621
			
1622
			InputStream oreInputStream = null;
1623
			try {
1624
				oreInputStream = this.get(session, potentialOreIdentifier);
1625
			} catch (NotFound nf) {
1626
				// this is probably okay for many sci meta data docs
1627
				logMetacat.warn("No potential ORE map found for: " + potentialOreIdentifier.getValue());
1628
				// try the SOLR index
1629
				List<Identifier> potentialOreIdentifiers = this.lookupOreFor(originalIdentifier, false);
1630
				if (potentialOreIdentifiers != null) {
1631
					potentialOreIdentifier = potentialOreIdentifiers.get(0);
1632
					try {
1633
						oreInputStream = this.get(session, potentialOreIdentifier);
1634
					} catch (NotFound nf2) {
1635
						// this is probably okay for many sci meta data docs
1636
						logMetacat.warn("No potential ORE map found for: " + potentialOreIdentifier.getValue());
1637
					}
1638
				}
1639
			}
1640
			if (oreInputStream != null) {
1641
				Identifier newOreIdentifier = MNodeService.getInstance(request).generateIdentifier(session, MNodeService.UUID_SCHEME, null);
1642
	
1643
				Map<Identifier, Map<Identifier, List<Identifier>>> resourceMapStructure = ResourceMapFactory.getInstance().parseResourceMap(oreInputStream);
1644
				Map<Identifier, List<Identifier>> sciMetaMap = resourceMapStructure.get(potentialOreIdentifier);
1645
				List<Identifier> dataIdentifiers = sciMetaMap.get(originalIdentifier);
1646
					
1647
				// reconstruct the ORE with the new identifiers
1648
				sciMetaMap.remove(originalIdentifier);
1649
				sciMetaMap.put(newIdentifier, dataIdentifiers);
1650
				
1651
				ResourceMap resourceMap = ResourceMapFactory.getInstance().createResourceMap(newOreIdentifier, sciMetaMap);
1652
				String resourceMapString = ResourceMapFactory.getInstance().serializeResourceMap(resourceMap);
1653
				
1654
				// get the original ORE SM and update the values
1655
				SystemMetadata originalOreSysMeta = this.getSystemMetadata(session, potentialOreIdentifier);
1656
				SystemMetadata oreSysMeta = new SystemMetadata();
1657
				try {
1658
					ByteArrayOutputStream baos = new ByteArrayOutputStream();
1659
					TypeMarshaller.marshalTypeToOutputStream(originalOreSysMeta, baos);
1660
					oreSysMeta = TypeMarshaller.unmarshalTypeFromStream(SystemMetadata.class, new ByteArrayInputStream(baos.toByteArray()));
1661
				} catch (Exception e) {
1662
					// report as service failure
1663
					ServiceFailure sf = new ServiceFailure("1030", e.getMessage());
1664
					sf.initCause(e);
1665
					throw sf;
1666
				}
1667

    
1668
				oreSysMeta.setIdentifier(newOreIdentifier);
1669
				oreSysMeta.setObsoletes(potentialOreIdentifier);
1670
				oreSysMeta.setObsoletedBy(null);
1671
				oreSysMeta.setSize(BigInteger.valueOf(resourceMapString.getBytes("UTF-8").length));
1672
				oreSysMeta.setChecksum(ChecksumUtil.checksum(resourceMapString.getBytes("UTF-8"), oreSysMeta.getChecksum().getAlgorithm()));
1673
				
1674
				// ensure ORE is publicly readable
1675
				oreSysMeta = makePublicIfNot(oreSysMeta, potentialOreIdentifier);
1676
				
1677
				// ensure all data objects allow public read
1678
				List<String> pidsToSync = new ArrayList<String>();
1679
				for (Identifier dataId: dataIdentifiers) {
1680
					SystemMetadata dataSysMeta = this.getSystemMetadata(session, dataId);
1681
					dataSysMeta = makePublicIfNot(dataSysMeta, dataId);
1682
					this.updateSystemMetadata(dataSysMeta);
1683
					pidsToSync.add(dataId.getValue());
1684
				}
1685
				SyncAccessPolicy sap = new SyncAccessPolicy();
1686
				try {
1687
					sap.sync(pidsToSync);
1688
				} catch (Exception e) {
1689
					// ignore
1690
					logMetacat.warn("Error attempting to sync access for data objects when publishing package");
1691
				}
1692
				
1693
				// save the updated ORE
1694
				this.update(
1695
						session, 
1696
						potentialOreIdentifier, 
1697
						new ByteArrayInputStream(resourceMapString.getBytes("UTF-8")), 
1698
						newOreIdentifier, 
1699
						oreSysMeta);
1700
				
1701
			} else {
1702
				// create a new ORE for them
1703
				// https://projects.ecoinformatics.org/ecoinfo/issues/6194
1704
				try {
1705
					// find the local id for the NEW package.
1706
					String newLocalId = IdentifierManager.getInstance().getLocalId(newIdentifier.getValue());
1707
	
1708
					@SuppressWarnings("unused")
1709
					SystemMetadata extraSysMeta = SystemMetadataFactory.createSystemMetadata(newLocalId, true, false);
1710
					// should be done generating the ORE here, and the same permissions were used from the metadata object
1711
					
1712
				} catch (Exception e) {
1713
					// oops, guess there was a problem - no package for you
1714
					logMetacat.error("Could not generate new ORE for published object: " + newIdentifier.getValue(), e);
1715
				}
1716
			}
1717
		} catch (McdbDocNotFoundException e) {
1718
			// report as service failure
1719
			ServiceFailure sf = new ServiceFailure("1030", e.getMessage());
1720
			sf.initCause(e);
1721
			throw sf;
1722
		} catch (UnsupportedEncodingException e) {
1723
			// report as service failure
1724
			ServiceFailure sf = new ServiceFailure("1030", e.getMessage());
1725
			sf.initCause(e);
1726
			throw sf;
1727
		} catch (OREException e) {
1728
			// report as service failure
1729
			ServiceFailure sf = new ServiceFailure("1030", e.getMessage());
1730
			sf.initCause(e);
1731
			throw sf;
1732
		} catch (URISyntaxException e) {
1733
			// report as service failure
1734
			ServiceFailure sf = new ServiceFailure("1030", e.getMessage());
1735
			sf.initCause(e);
1736
			throw sf;
1737
		} catch (OREParserException e) {
1738
			// report as service failure
1739
			ServiceFailure sf = new ServiceFailure("1030", e.getMessage());
1740
			sf.initCause(e);
1741
			throw sf;
1742
		} catch (ORESerialiserException e) {
1743
			// report as service failure
1744
			ServiceFailure sf = new ServiceFailure("1030", e.getMessage());
1745
			sf.initCause(e);
1746
			throw sf;
1747
		} catch (NoSuchAlgorithmException e) {
1748
			// report as service failure
1749
			ServiceFailure sf = new ServiceFailure("1030", e.getMessage());
1750
			sf.initCause(e);
1751
			throw sf;
1752
		} catch (SQLException e) {
1753
            // report as service failure
1754
            ServiceFailure sf = new ServiceFailure("1030", e.getMessage());
1755
            sf.initCause(e);
1756
            throw sf;
1757
        }
1758
		
1759
		return newIdentifier;
1760
	}
1761
	
1762
	/**
1763
	 * Determines if we already have registered an ORE map for this package
1764
	 * NOTE: uses a solr query to locate OREs for the object
1765
	 * @param guid of the EML/packaging object
1766
	 * @return list of resource map identifiers for the given pid
1767
	 */
1768
	public List<Identifier> lookupOreFor(Identifier guid, boolean includeObsolete) {
1769
		// Search for the ORE if we can find it
1770
		String pid = guid.getValue();
1771
		List<Identifier> retList = null;
1772
		try {
1773
			String query = "fl=id,resourceMap&wt=xml&q=-obsoletedBy:[* TO *]+resourceMap:[* TO *]+id:\"" + pid + "\"";
1774
			if (includeObsolete) {
1775
				query = "fl=id,resourceMap&wt=xml&q=resourceMap:[* TO *]+id:\"" + pid + "\"";
1776
			}
1777
			
1778
			InputStream results = this.query(null, "solr", query);
1779
			org.w3c.dom.Node rootNode = XMLUtilities.getXMLReaderAsDOMTreeRootNode(new InputStreamReader(results, "UTF-8"));
1780
			//String resultString = XMLUtilities.getDOMTreeAsString(rootNode);
1781
			org.w3c.dom.NodeList nodeList = XMLUtilities.getNodeListWithXPath(rootNode, "//arr[@name=\"resourceMap\"]/str");
1782
			if (nodeList != null && nodeList.getLength() > 0) {
1783
				retList = new ArrayList<Identifier>();
1784
				for (int i = 0; i < nodeList.getLength(); i++) {
1785
					String found = nodeList.item(i).getFirstChild().getNodeValue();
1786
					Identifier oreId = new Identifier();
1787
					oreId.setValue(found);
1788
					retList.add(oreId);
1789
				}
1790
			}
1791
		} catch (Exception e) {
1792
			logMetacat.error("Error checking for resourceMap[s] on pid " + pid + ". " + e.getMessage(), e);
1793
		}
1794
		
1795
		return retList;
1796
	}
1797
	
1798

    
1799
	@Override
1800
	public InputStream getPackage(Session session, ObjectFormatIdentifier formatId,
1801
			Identifier pid) throws InvalidToken, ServiceFailure,
1802
			NotAuthorized, InvalidRequest, NotImplemented, NotFound {
1803
		InputStream bagInputStream = null;
1804
		BagFactory bagFactory = new BagFactory();
1805
		Bag bag = bagFactory.createBag();
1806
		
1807
		// track the temp files we use so we can delete them when finished
1808
		List<File> tempFiles = new ArrayList<File>();
1809
		
1810
		// the pids to include in the package
1811
		List<Identifier> packagePids = new ArrayList<Identifier>();
1812
		
1813
		// catch non-D1 service errors and throw as ServiceFailures
1814
		try {
1815
			//Create a map of dataone ids and file names
1816
			Map<Identifier, String> fileNames = new HashMap<Identifier, String>();
1817
			
1818
			// track the pid-to-file mapping
1819
			StringBuffer pidMapping = new StringBuffer();
1820
			
1821
			// find the package contents
1822
			SystemMetadata sysMeta = this.getSystemMetadata(session, pid);
1823
			if (ObjectFormatCache.getInstance().getFormat(sysMeta.getFormatId()).getFormatType().equals("RESOURCE")) {
1824
				//Get the resource map as a map of Identifiers
1825
				InputStream oreInputStream = this.get(session, pid);
1826
				Map<Identifier, Map<Identifier, List<Identifier>>> resourceMapStructure = ResourceMapFactory.getInstance().parseResourceMap(oreInputStream);
1827
				packagePids.addAll(resourceMapStructure.keySet());
1828
				//Loop through each object in this resource map
1829
				for (Map<Identifier, List<Identifier>> entries: resourceMapStructure.values()) {
1830
					//Loop through each metadata object in this entry
1831
					Set<Identifier> metadataIdentifiers = entries.keySet();
1832
					for(Identifier metadataID: metadataIdentifiers){
1833
						try{
1834
							//Get the system metadata for this metadata object
1835
							SystemMetadata metadataSysMeta = this.getSystemMetadata(session, metadataID);
1836
							
1837
							// include user-friendly metadata
1838
							if (ObjectFormatCache.getInstance().getFormat(metadataSysMeta.getFormatId()).getFormatType().equals("METADATA")) {
1839
								InputStream metadataStream = this.get(session, metadataID);
1840
							
1841
								try {
1842
									// transform
1843
						            String format = "default";
1844

    
1845
									DBTransform transformer = new DBTransform();
1846
						            String documentContent = IOUtils.toString(metadataStream, "UTF-8");
1847
						            String sourceType = metadataSysMeta.getFormatId().getValue();
1848
						            String targetType = "-//W3C//HTML//EN";
1849
						            ByteArrayOutputStream baos = new ByteArrayOutputStream();
1850
						            Writer writer = new OutputStreamWriter(baos , "UTF-8");
1851
						            // TODO: include more params?
1852
						            Hashtable<String, String[]> params = new Hashtable<String, String[]>();
1853
						            String localId = null;
1854
									try {
1855
										localId = IdentifierManager.getInstance().getLocalId(pid.getValue());
1856
									} catch (McdbDocNotFoundException e) {
1857
										throw new NotFound("1020", e.getMessage());
1858
									}
1859
									params.put("qformat", new String[] {format});	            
1860
						            params.put("docid", new String[] {localId});
1861
						            params.put("pid", new String[] {pid.getValue()});
1862
						            params.put("displaymodule", new String[] {"printall"});
1863
						            
1864
						            transformer.transformXMLDocument(
1865
						                    documentContent , 
1866
						                    sourceType, 
1867
						                    targetType , 
1868
						                    format, 
1869
						                    writer, 
1870
						                    params, 
1871
						                    null //sessionid
1872
						                    );
1873
						            
1874
						            // finally, get the HTML back
1875
						            ContentTypeByteArrayInputStream resultInputStream = new ContentTypeByteArrayInputStream(baos.toByteArray());
1876
						            
1877
						            // write to temp file with correct css path
1878
						            File tmpDir = File.createTempFile("package_", "_dir");
1879
						            tmpDir.delete();
1880
						            tmpDir.mkdir();
1881
						            File htmlFile = File.createTempFile("metadata", ".html", tmpDir);
1882
						            File cssDir = new File(tmpDir, format);
1883
						            cssDir.mkdir();
1884
						            File cssFile = new File(tmpDir, format + "/" + format + ".css");
1885
						            String pdfFileName = metadataID.getValue().replaceAll("[^a-zA-Z0-9\\-\\.]", "_") + "-METADATA.pdf";
1886
						            File pdfFile = new File(tmpDir, pdfFileName);
1887
						            //File pdfFile = File.createTempFile("metadata", ".pdf", tmpDir);
1888
						            
1889
						            // put the CSS file in place for the html to find it
1890
						            String originalCssPath = SystemUtil.getContextDir() + "/style/skins/" + format + "/" + format + ".css";
1891
						            IOUtils.copy(new FileInputStream(originalCssPath), new FileOutputStream(cssFile));
1892
						            
1893
						            // write the HTML file
1894
						            IOUtils.copy(resultInputStream, new FileOutputStream(htmlFile));
1895
						            
1896
						            // convert to PDF
1897
						            HtmlToPdf.export(htmlFile.getAbsolutePath(), pdfFile.getAbsolutePath());
1898
						            
1899
						            //add to the package
1900
						            bag.addFileToPayload(pdfFile);
1901
									pidMapping.append(metadataID.getValue() + " (pdf)" +  "\t" + "data/" + pdfFile.getName() + "\n");
1902
						            
1903
						            // mark for clean up after we are done
1904
									htmlFile.delete();
1905
									cssFile.delete();
1906
									cssDir.delete();
1907
						            tempFiles.add(tmpDir);
1908
									tempFiles.add(pdfFile); // delete this first later on
1909
						            
1910
								} catch (Exception e) {
1911
									logMetacat.warn("Could not transform metadata", e);
1912
								}
1913
							}
1914

    
1915
							
1916
							//If this is in eml format, extract the filename and GUID from each entity in its package
1917
							if (metadataSysMeta.getFormatId().getValue().startsWith("eml://")) {
1918
								//Get the package
1919
								DataPackageParserInterface parser = new Eml200DataPackageParser();
1920
								InputStream emlStream = this.get(session, metadataID);
1921
								parser.parse(emlStream);
1922
								DataPackage dataPackage = parser.getDataPackage();
1923
								
1924
								//Get all the entities in this package and loop through each to extract its ID and file name
1925
								Entity[] entities = dataPackage.getEntityList();
1926
								for(Entity entity: entities){
1927
									try{
1928
										//Get the file name from the metadata
1929
										String fileNameFromMetadata = entity.getName();
1930
										
1931
										//Get the ecogrid URL from the metadata
1932
										String ecogridIdentifier = entity.getEntityIdentifier();
1933
										//Parse the ecogrid URL to get the local id
1934
										String idFromMetadata = DocumentUtil.getAccessionNumberFromEcogridIdentifier(ecogridIdentifier);
1935
										
1936
										//Get the docid and rev pair
1937
										String docid = DocumentUtil.getDocIdFromString(idFromMetadata);
1938
										String rev = DocumentUtil.getRevisionStringFromString(idFromMetadata);
1939
										
1940
										//Get the GUID
1941
										String guid = IdentifierManager.getInstance().getGUID(docid, Integer.valueOf(rev));
1942
										Identifier dataIdentifier = new Identifier();
1943
										dataIdentifier.setValue(guid);
1944
										
1945
										//Add the GUID to our GUID & file name map
1946
										fileNames.put(dataIdentifier, fileNameFromMetadata);
1947
									}
1948
									catch(Exception e){
1949
										//Prevent just one entity error
1950
										e.printStackTrace();
1951
										logMetacat.debug(e.getMessage(), e);
1952
									}
1953
								}
1954
							}
1955
						}
1956
						catch(Exception e){
1957
							//Catch errors that would prevent package download
1958
							logMetacat.debug(e.toString());
1959
						}
1960
					}
1961
					packagePids.addAll(entries.keySet());
1962
					for (List<Identifier> dataPids: entries.values()) {
1963
						packagePids.addAll(dataPids);
1964
					}
1965
				}
1966
			} else {
1967
				// just the lone pid in this package
1968
				packagePids.add(pid);
1969
			}
1970
			
1971
			//Create a temp file, then delete it and make a directory with that name
1972
			File tempDir = File.createTempFile("temp", Long.toString(System.nanoTime()));
1973
			tempDir.delete();
1974
			tempDir = new File(tempDir.getPath() + "_dir");
1975
			tempDir.mkdir();			
1976
			tempFiles.add(tempDir);
1977
			File pidMappingFile = new File(tempDir, "pid-mapping.txt");
1978
			
1979
			// loop through the package contents
1980
			for (Identifier entryPid: packagePids) {
1981
				//Get the system metadata for each item
1982
				SystemMetadata entrySysMeta = this.getSystemMetadata(session, entryPid);					
1983
				
1984
				String objectFormatType = ObjectFormatCache.getInstance().getFormat(entrySysMeta.getFormatId()).getFormatType();
1985
				String fileName = null;
1986
				
1987
				//TODO: Be more specific of what characters to replace. Make sure periods arent replaced for the filename from metadata
1988
				//Our default file name is just the ID + format type (e.g. walker.1.1-DATA)
1989
				fileName = entryPid.getValue().replaceAll("[^a-zA-Z0-9\\-\\.]", "_") + "-" + objectFormatType;
1990

    
1991
				if(fileNames.containsKey(entryPid)){
1992
					//Let's use the file name and extension from the metadata is we have it
1993
					fileName = entryPid.getValue().replaceAll("[^a-zA-Z0-9\\-\\.]", "_") + "-" + fileNames.get(entryPid).replaceAll("[^a-zA-Z0-9\\-\\.]", "_");
1994
				}
1995
				else{
1996
					//If we couldn't find a given file name, use the system metadata extension
1997
					String extension = ObjectFormatInfo.instance().getExtension(entrySysMeta.getFormatId().getValue());
1998
					fileName += extension;
1999
				}
2000
				
2001
		        //Create a new file for this item and add to the list
2002
				File tempFile = new File(tempDir, fileName);
2003
				tempFiles.add(tempFile);
2004
				
2005
				InputStream entryInputStream = this.get(session, entryPid);			
2006
				IOUtils.copy(entryInputStream, new FileOutputStream(tempFile));
2007
				bag.addFileToPayload(tempFile);
2008
				pidMapping.append(entryPid.getValue() + "\t" + "data/" + tempFile.getName() + "\n");
2009
			}
2010
			
2011
			//add the the pid to data file map
2012
			IOUtils.write(pidMapping.toString(), new FileOutputStream(pidMappingFile));
2013
			bag.addFileAsTag(pidMappingFile);
2014
			tempFiles.add(pidMappingFile);
2015
			
2016
			bag = bag.makeComplete();
2017
			
2018
			///Now create the zip file
2019
			//Use the pid as the file name prefix, replacing all non-word characters
2020
			String zipName = pid.getValue().replaceAll("\\W", "_");
2021
			
2022
			File bagFile = new File(tempDir, zipName+".zip");
2023
			
2024
			bag.setFile(bagFile);
2025
			ZipWriter zipWriter = new ZipWriter(bagFactory);
2026
			bag.write(zipWriter, bagFile);
2027
			bagFile = bag.getFile();
2028
			// use custom FIS that will delete the file when closed
2029
			bagInputStream = new DeleteOnCloseFileInputStream(bagFile);
2030
			// also mark for deletion on shutdown in case the stream is never closed
2031
			bagFile.deleteOnExit();
2032
			tempFiles.add(bagFile);
2033
			
2034
			// clean up other temp files
2035
			for (int i=tempFiles.size()-1; i>=0; i--){
2036
				tempFiles.get(i).delete();
2037
			}
2038
			
2039
		} catch (IOException e) {
2040
			// report as service failure
2041
			ServiceFailure sf = new ServiceFailure("1030", e.getMessage());
2042
			sf.initCause(e);
2043
			throw sf;
2044
		} catch (OREException e) {
2045
			// report as service failure
2046
			ServiceFailure sf = new ServiceFailure("1030", e.getMessage());
2047
			sf.initCause(e);
2048
			throw sf;
2049
		} catch (URISyntaxException e) {
2050
			// report as service failure
2051
			ServiceFailure sf = new ServiceFailure("1030", e.getMessage());
2052
			sf.initCause(e);
2053
			throw sf;
2054
		} catch (OREParserException e) {
2055
			// report as service failure
2056
			ServiceFailure sf = new ServiceFailure("1030", e.getMessage());
2057
			sf.initCause(e);
2058
			throw sf;
2059
		}
2060
		
2061
		return bagInputStream;
2062
	}
2063

    
2064
	@Override
2065
	public OptionList listViews(Session arg0) throws InvalidToken,
2066
			ServiceFailure, NotAuthorized, InvalidRequest, NotImplemented {
2067
		OptionList views = new OptionList();
2068
		Vector<String> skinNames = null;
2069
		try {
2070
			skinNames = SkinUtil.getSkinNames();
2071
		} catch (PropertyNotFoundException e) {
2072
			throw new ServiceFailure("2841", e.getMessage());
2073
		}
2074
		for (String skinName: skinNames) {
2075
			views.addOption(skinName);
2076
		}
2077
		return views;
2078
	}
2079

    
2080
	@Override
2081
	public InputStream view(Session session, String format, Identifier pid)
2082
			throws InvalidToken, ServiceFailure, NotAuthorized, InvalidRequest,
2083
			NotImplemented, NotFound {
2084
		InputStream resultInputStream = null;
2085
		
2086
		SystemMetadata sysMeta = this.getSystemMetadata(session, pid);
2087
		InputStream object = this.get(session, pid);
2088

    
2089
		try {
2090
			// can only transform metadata, really
2091
			ObjectFormat objectFormat = ObjectFormatCache.getInstance().getFormat(sysMeta.getFormatId());
2092
			if (objectFormat.getFormatType().equals("METADATA")) {
2093
				// transform
2094
				DBTransform transformer = new DBTransform();
2095
	            String documentContent = IOUtils.toString(object, "UTF-8");
2096
	            String sourceType = objectFormat.getFormatId().getValue();
2097
	            String targetType = "-//W3C//HTML//EN";
2098
	            ByteArrayOutputStream baos = new ByteArrayOutputStream();
2099
	            Writer writer = new OutputStreamWriter(baos , "UTF-8");
2100
	            // TODO: include more params?
2101
	            Hashtable<String, String[]> params = new Hashtable<String, String[]>();
2102
	            String localId = null;
2103
				try {
2104
					localId = IdentifierManager.getInstance().getLocalId(pid.getValue());
2105
				} catch (McdbDocNotFoundException e) {
2106
					throw new NotFound("1020", e.getMessage());
2107
				}
2108
	            params.put("qformat", new String[] {format});	            
2109
	            params.put("docid", new String[] {localId});
2110
	            params.put("pid", new String[] {pid.getValue()});
2111
	            transformer.transformXMLDocument(
2112
	                    documentContent , 
2113
	                    sourceType, 
2114
	                    targetType , 
2115
	                    format, 
2116
	                    writer, 
2117
	                    params, 
2118
	                    null //sessionid
2119
	                    );
2120
	            
2121
	            // finally, get the HTML back
2122
	            resultInputStream = new ContentTypeByteArrayInputStream(baos.toByteArray());
2123
	            ((ContentTypeByteArrayInputStream) resultInputStream).setContentType("text/html");
2124
	
2125
			} else {
2126
				// just return the raw bytes
2127
				resultInputStream = object;
2128
			}
2129
		} catch (IOException e) {
2130
			// report as service failure
2131
			ServiceFailure sf = new ServiceFailure("1030", e.getMessage());
2132
			sf.initCause(e);
2133
			throw sf;
2134
		} catch (PropertyNotFoundException e) {
2135
			// report as service failure
2136
			ServiceFailure sf = new ServiceFailure("1030", e.getMessage());
2137
			sf.initCause(e);
2138
			throw sf;
2139
		} catch (SQLException e) {
2140
			// report as service failure
2141
			ServiceFailure sf = new ServiceFailure("1030", e.getMessage());
2142
			sf.initCause(e);
2143
			throw sf;
2144
		} catch (ClassNotFoundException e) {
2145
			// report as service failure
2146
			ServiceFailure sf = new ServiceFailure("1030", e.getMessage());
2147
			sf.initCause(e);
2148
			throw sf;
2149
		}
2150
		
2151
		return resultInputStream;
2152
	}	
2153
    
2154
}
(4-4/7)