• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IQSS / dataverse / #21824

20 Mar 2024 08:05PM CUT coverage: 20.661% (+0.09%) from 20.57%
#21824

push

github

web-flow
Merge pull request #10211 from IQSS/9356-rate-limiting-command-engine

adding rate limiting for command engine

90 of 123 new or added lines in 14 files covered. (73.17%)

1 existing line in 1 file now uncovered.

17074 of 82639 relevant lines covered (20.66%)

0.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

13.75
/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java
1
package edu.harvard.iq.dataverse.api;
2

3
import edu.harvard.iq.dataverse.*;
4
import edu.harvard.iq.dataverse.actionlogging.ActionLogServiceBean;
5
import static edu.harvard.iq.dataverse.api.Datasets.handleVersion;
6
import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean;
7
import edu.harvard.iq.dataverse.authorization.DataverseRole;
8
import edu.harvard.iq.dataverse.authorization.RoleAssignee;
9
import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean;
10
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
11
import edu.harvard.iq.dataverse.authorization.users.User;
12
import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailServiceBean;
13
import edu.harvard.iq.dataverse.datacapturemodule.DataCaptureModuleServiceBean;
14
import edu.harvard.iq.dataverse.engine.command.Command;
15
import edu.harvard.iq.dataverse.engine.command.DataverseRequest;
16
import edu.harvard.iq.dataverse.engine.command.exception.CommandException;
17
import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException;
18
import edu.harvard.iq.dataverse.engine.command.exception.PermissionException;
19
import edu.harvard.iq.dataverse.engine.command.impl.GetDraftDatasetVersionCommand;
20
import edu.harvard.iq.dataverse.engine.command.impl.GetLatestAccessibleDatasetVersionCommand;
21
import edu.harvard.iq.dataverse.engine.command.impl.GetLatestPublishedDatasetVersionCommand;
22
import edu.harvard.iq.dataverse.engine.command.impl.GetSpecificPublishedDatasetVersionCommand;
23
import edu.harvard.iq.dataverse.engine.command.exception.RateLimitCommandException;
24
import edu.harvard.iq.dataverse.externaltools.ExternalToolServiceBean;
25
import edu.harvard.iq.dataverse.license.LicenseServiceBean;
26
import edu.harvard.iq.dataverse.locality.StorageSiteServiceBean;
27
import edu.harvard.iq.dataverse.metrics.MetricsServiceBean;
28
import edu.harvard.iq.dataverse.search.savedsearch.SavedSearchServiceBean;
29
import edu.harvard.iq.dataverse.settings.SettingsServiceBean;
30
import edu.harvard.iq.dataverse.util.BundleUtil;
31
import edu.harvard.iq.dataverse.util.FileUtil;
32
import edu.harvard.iq.dataverse.util.SystemConfig;
33
import edu.harvard.iq.dataverse.util.json.JsonParser;
34
import edu.harvard.iq.dataverse.util.json.JsonUtil;
35
import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder;
36
import edu.harvard.iq.dataverse.validation.PasswordValidatorServiceBean;
37
import jakarta.ejb.EJB;
38
import jakarta.ejb.EJBException;
39
import jakarta.json.*;
40
import jakarta.json.JsonValue.ValueType;
41
import jakarta.persistence.EntityManager;
42
import jakarta.persistence.NoResultException;
43
import jakarta.persistence.PersistenceContext;
44
import jakarta.servlet.http.HttpServletRequest;
45
import jakarta.ws.rs.container.ContainerRequestContext;
46
import jakarta.ws.rs.core.Context;
47
import jakarta.ws.rs.core.MediaType;
48
import jakarta.ws.rs.core.Response;
49
import jakarta.ws.rs.core.Response.ResponseBuilder;
50
import jakarta.ws.rs.core.Response.Status;
51

52
import java.io.InputStream;
53
import java.net.URI;
54
import java.util.Arrays;
55
import java.util.Collections;
56
import java.util.UUID;
57
import java.util.concurrent.Callable;
58
import java.util.logging.Level;
59
import java.util.logging.Logger;
60

61
import static org.apache.commons.lang3.StringUtils.isNumeric;
62

63
/**
64
 * Base class for API beans
65
 * @author michael
66
 */
67
public abstract class AbstractApiBean {
1✔
68

69
    private static final Logger logger = Logger.getLogger(AbstractApiBean.class.getName());
1✔
70
    private static final String DATAVERSE_KEY_HEADER_NAME = "X-Dataverse-key";
71
    private static final String PERSISTENT_ID_KEY=":persistentId";
72
    private static final String ALIAS_KEY=":alias";
73
    public static final String STATUS_WF_IN_PROGRESS = "WORKFLOW_IN_PROGRESS";
74
    public static final String DATAVERSE_WORKFLOW_INVOCATION_HEADER_NAME = "X-Dataverse-invocationID";
75
    public static final String RESPONSE_MESSAGE_AUTHENTICATED_USER_REQUIRED = "Only authenticated users can perform the requested operation";
76

77
    /**
78
     * Utility class to convey a proper error response using Java's exceptions.
79
     */
80
    public static class WrappedResponse extends Exception {
81
        private final Response response;
82

83
        public WrappedResponse(Response response) {
1✔
84
            this.response = response;
1✔
85
        }
1✔
86

87
        public WrappedResponse( Throwable cause, Response response ) {
88
            super( cause );
×
89
            this.response = response;
×
90
        }
×
91

92
        public Response getResponse() {
93
            return response;
1✔
94
        }
95

96
        /**
97
         * Creates a new response, based on the original response and the passed message.
98
         * Typical use would be to add a better error message to the HTTP response.
99
         * @param message additional message to be added to the response.
100
         * @return A Response with updated message field.
101
         */
102
        public Response refineResponse( String message ) {
103
            final Status statusCode = Response.Status.fromStatusCode(response.getStatus());
×
104
            String baseMessage = getWrappedMessageWhenJson();
×
105

106
            if ( baseMessage == null ) {
×
107
                final Throwable cause = getCause();
×
108
                baseMessage = (cause!=null ? cause.getMessage() : "");
×
109
            }
110
            return error(statusCode, message+" "+baseMessage);
×
111
        }
112

113
        /**
114
         * In the common case of the wrapped response being of type JSON,
115
         * return the message field it has (if any).
116
         * @return the content of a message field, or {@code null}.
117
         * @throws JsonException when JSON parsing fails.
118
         */
119
        String getWrappedMessageWhenJson() {
120
            if ( response.getMediaType().equals(MediaType.APPLICATION_JSON_TYPE) ) {
×
121
                Object entity = response.getEntity();
×
122
                if ( entity == null ) return null;
×
123

124
                JsonObject obj = JsonUtil.getJsonObject(entity.toString());
×
125
                if ( obj.containsKey("message") ) {
×
126
                    JsonValue message = obj.get("message");
×
127
                    return message.getValueType() == ValueType.STRING ? obj.getString("message") : message.toString();
×
128
                } else {
129
                    return null;
×
130
                }
131

132
            } else {
133
                return null;
×
134
            }
135
        }
136
    }
137

138
    @EJB
139
    protected EjbDataverseEngine engineSvc;
140

141
    @EJB
142
    protected DvObjectServiceBean dvObjectSvc;
143
    
144
    @EJB
145
    protected DatasetServiceBean datasetSvc;
146
    
147
    @EJB
148
    protected DataFileServiceBean fileService;
149

150
    @EJB
151
    protected DataverseServiceBean dataverseSvc;
152

153
    @EJB
154
    protected AuthenticationServiceBean authSvc;
155

156
    @EJB
157
    protected DatasetFieldServiceBean datasetFieldSvc;
158

159
    @EJB
160
    protected MetadataBlockServiceBean metadataBlockSvc;
161

162
    @EJB
163
    protected LicenseServiceBean licenseSvc;
164

165
    @EJB
166
    protected UserServiceBean userSvc;
167

168
        @EJB
169
        protected DataverseRoleServiceBean rolesSvc;
170

171
    @EJB
172
    protected SettingsServiceBean settingsSvc;
173

174
    @EJB
175
    protected RoleAssigneeServiceBean roleAssigneeSvc;
176

177
    @EJB
178
    protected PermissionServiceBean permissionSvc;
179

180
    @EJB
181
    protected GroupServiceBean groupSvc;
182

183
    @EJB
184
    protected ActionLogServiceBean actionLogSvc;
185

186
    @EJB
187
    protected SavedSearchServiceBean savedSearchSvc;
188

189
    @EJB
190
    protected ConfirmEmailServiceBean confirmEmailSvc;
191

192
    @EJB
193
    protected UserNotificationServiceBean userNotificationSvc;
194

195
    @EJB
196
    protected DatasetVersionServiceBean datasetVersionSvc;
197

198
    @EJB
199
    protected SystemConfig systemConfig;
200

201
    @EJB
202
    protected DataCaptureModuleServiceBean dataCaptureModuleSvc;
203
    
204
    @EJB
205
    protected DatasetLinkingServiceBean dsLinkingService;
206
    
207
    @EJB
208
    protected DataverseLinkingServiceBean dvLinkingService;
209

210
    @EJB
211
    protected PasswordValidatorServiceBean passwordValidatorService;
212

213
    @EJB
214
    protected ExternalToolServiceBean externalToolService;
215

216
    @EJB
217
    DataFileServiceBean fileSvc;
218

219
    @EJB
220
    StorageSiteServiceBean storageSiteSvc;
221

222
    @EJB
223
    MetricsServiceBean metricsSvc;
224
    
225
    @EJB 
226
    DvObjectServiceBean dvObjSvc;
227
    
228
    @EJB 
229
    GuestbookResponseServiceBean gbRespSvc;
230

231
    @PersistenceContext(unitName = "VDCNet-ejbPU")
232
    protected EntityManager em;
233

234
    @Context
235
    protected HttpServletRequest httpRequest;
236

237
    /**
238
     * For pretty printing (indenting) of JSON output.
239
     */
240
    public enum Format {
×
241

242
        PRETTY
×
243
    }
244

245
    private final LazyRef<JsonParser> jsonParserRef = new LazyRef<>(new Callable<JsonParser>() {
1✔
246
        @Override
247
        public JsonParser call() throws Exception {
248
            return new JsonParser(datasetFieldSvc, metadataBlockSvc,settingsSvc, licenseSvc);
×
249
        }
250
    });
251

252
    /**
253
     * Functional interface for handling HTTP requests in the APIs.
254
     *
255
     * @see #response(edu.harvard.iq.dataverse.api.AbstractApiBean.DataverseRequestHandler, edu.harvard.iq.dataverse.authorization.users.User)
256
     */
257
    protected static interface DataverseRequestHandler {
258
        Response handle( DataverseRequest u ) throws WrappedResponse;
259
    }
260

261

262
    /* ===================== *\
263
     *  Utility Methods      *
264
     *  Get that DSL feelin' *
265
    \* ===================== */
266

267
    protected JsonParser jsonParser() {
268
        return jsonParserRef.get();
×
269
    }
270

271
    protected boolean parseBooleanOrDie( String input ) throws WrappedResponse {
272
        if (input == null ) throw new WrappedResponse( badRequest("Boolean value missing"));
1✔
273
        input = input.trim();
1✔
274
        if ( Util.isBoolean(input) ) {
1✔
275
            return Util.isTrue(input);
1✔
276
        } else {
277
            throw new WrappedResponse( badRequest("Illegal boolean value '" + input + "'"));
1✔
278
        }
279
    }
280

281
     /**
282
     * Returns the {@code key} query parameter from the current request, or {@code null} if
283
     * the request has no such parameter.
284
     * @param key Name of the requested parameter.
285
     * @return Value of the requested parameter in the current request.
286
     */
287
    protected String getRequestParameter( String key ) {
288
        return httpRequest.getParameter(key);
×
289
    }
290

291
    protected String getRequestApiKey() {
292
        String headerParamApiKey = httpRequest.getHeader(DATAVERSE_KEY_HEADER_NAME);
×
293
        String queryParamApiKey = httpRequest.getParameter("key");
×
294
                
295
        return headerParamApiKey!=null ? headerParamApiKey : queryParamApiKey;
×
296
    }
297

298
    protected User getRequestUser(ContainerRequestContext crc) {
299
        return (User) crc.getProperty(ApiConstants.CONTAINER_REQUEST_CONTEXT_USER);
1✔
300
    }
301

302
    /**
303
     * Gets the authenticated user from the ContainerRequestContext user property. If the user from the property
304
     * is not authenticated, throws a wrapped "authenticated user required" user (HTTP UNAUTHORIZED) response.
305
     * @param crc a ContainerRequestContext implementation
306
     * @return The authenticated user
307
     * @throws edu.harvard.iq.dataverse.api.AbstractApiBean.WrappedResponse in case the user is not authenticated.
308
     *
309
     * TODO:
310
     *  This method is designed to comply with existing authorization logic, based on the old findAuthenticatedUserOrDie method.
311
     *  Ideally, as for authentication, a filter could be implemented for authorization, which would extract and encapsulate the
312
     *  authorization logic from the AbstractApiBean.
313
     */
314
    protected AuthenticatedUser getRequestAuthenticatedUserOrDie(ContainerRequestContext crc) throws WrappedResponse {
315
        User requestUser = (User) crc.getProperty(ApiConstants.CONTAINER_REQUEST_CONTEXT_USER);
×
316
        if (requestUser.isAuthenticated()) {
×
317
            return (AuthenticatedUser) requestUser;
×
318
        } else {
319
            throw new WrappedResponse(authenticatedUserRequired());
×
320
        }
321
    }
322

323
    /* ========= *\
324
     *  Finders  *
325
    \* ========= */
326
    protected RoleAssignee findAssignee(String identifier) {
327
        try {
328
            RoleAssignee roleAssignee = roleAssigneeSvc.getRoleAssignee(identifier);
×
329
            return roleAssignee;
×
330
        } catch (EJBException ex) {
×
331
            Throwable cause = ex;
×
332
            while (cause.getCause() != null) {
×
333
                cause = cause.getCause();
×
334
            }
335
            logger.log(Level.INFO, "Exception caught looking up RoleAssignee based on identifier ''{0}'': {1}", new Object[]{identifier, cause.getMessage()});
×
336
            return null;
×
337
        }
338
    }
339

340
    /**
341
     * @param apiKey the key to find the user with
342
     * @return the user, or null
343
     */
344
    protected AuthenticatedUser findUserByApiToken( String apiKey ) {
345
        return authSvc.lookupUser(apiKey);
×
346
    }
347

348
    protected Dataverse findDataverseOrDie( String dvIdtf ) throws WrappedResponse {
349
        Dataverse dv = findDataverse(dvIdtf);
1✔
350
        if ( dv == null ) {
1✔
351
            throw new WrappedResponse(error( Response.Status.NOT_FOUND, "Can't find dataverse with identifier='" + dvIdtf + "'"));
1✔
352
        }
353
        return dv;
1✔
354
    }
355
    
356
    protected DataverseLinkingDataverse findDataverseLinkingDataverseOrDie(String dataverseId, String linkedDataverseId) throws WrappedResponse {
357
        DataverseLinkingDataverse dvld;
358
        Dataverse dataverse = findDataverseOrDie(dataverseId);
×
359
        Dataverse linkedDataverse = findDataverseOrDie(linkedDataverseId);
×
360
        try {
361
            dvld = dvLinkingService.findDataverseLinkingDataverse(dataverse.getId(), linkedDataverse.getId());
×
362
            if (dvld == null) {
×
363
                throw new WrappedResponse(notFound(BundleUtil.getStringFromBundle("find.dataverselinking.error.not.found.ids", Arrays.asList(dataverseId, linkedDataverseId))));
×
364
            }
365
            return dvld;
×
366
        } catch (NumberFormatException nfe) {
×
367
            throw new WrappedResponse(
×
368
                    badRequest(BundleUtil.getStringFromBundle("find.dataverselinking.error.not.found.bad.ids", Arrays.asList(dataverseId, linkedDataverseId))));
×
369
        }
370
    }
371

372
    protected Dataset findDatasetOrDie(String id) throws WrappedResponse {
373
        Dataset dataset;
374
        if (id.equals(PERSISTENT_ID_KEY)) {
×
375
            String persistentId = getRequestParameter(PERSISTENT_ID_KEY.substring(1));
×
376
            if (persistentId == null) {
×
377
                throw new WrappedResponse(
×
378
                        badRequest(BundleUtil.getStringFromBundle("find.dataset.error.dataset_id_is_null", Collections.singletonList(PERSISTENT_ID_KEY.substring(1)))));
×
379
            }
380
            dataset = datasetSvc.findByGlobalId(persistentId);
×
381
            if (dataset == null) {
×
382
                throw new WrappedResponse(notFound(BundleUtil.getStringFromBundle("find.dataset.error.dataset.not.found.persistentId", Collections.singletonList(persistentId))));
×
383
            }
384
            return dataset;
×
385

386
        } else {
387
            try {
388
                dataset = datasetSvc.find(Long.parseLong(id));
×
389
                if (dataset == null) {
×
390
                    throw new WrappedResponse(notFound(BundleUtil.getStringFromBundle("find.dataset.error.dataset.not.found.id", Collections.singletonList(id))));
×
391
                }
392
                return dataset;
×
393
            } catch (NumberFormatException nfe) {
×
394
                throw new WrappedResponse(
×
395
                        badRequest(BundleUtil.getStringFromBundle("find.dataset.error.dataset.not.found.bad.id", Collections.singletonList(id))));
×
396
            }
397
        }
398
    }
399

400
    protected DatasetVersion findDatasetVersionOrDie(final DataverseRequest req, String versionNumber, final Dataset ds, boolean includeDeaccessioned, boolean checkPermsWhenDeaccessioned) throws WrappedResponse {
401
        DatasetVersion dsv = execCommand(handleVersion(versionNumber, new Datasets.DsVersionHandler<Command<DatasetVersion>>() {
×
402

403
            @Override
404
            public Command<DatasetVersion> handleLatest() {
405
                return new GetLatestAccessibleDatasetVersionCommand(req, ds, includeDeaccessioned, checkPermsWhenDeaccessioned);
×
406
            }
407

408
            @Override
409
            public Command<DatasetVersion> handleDraft() {
410
                return new GetDraftDatasetVersionCommand(req, ds);
×
411
            }
412

413
            @Override
414
            public Command<DatasetVersion> handleSpecific(long major, long minor) {
415
                return new GetSpecificPublishedDatasetVersionCommand(req, ds, major, minor, includeDeaccessioned, checkPermsWhenDeaccessioned);
×
416
            }
417

418
            @Override
419
            public Command<DatasetVersion> handleLatestPublished() {
420
                return new GetLatestPublishedDatasetVersionCommand(req, ds, includeDeaccessioned, checkPermsWhenDeaccessioned);
×
421
            }
422
        }));
423
        return dsv;
×
424
    }
425

426
    protected DataFile findDataFileOrDie(String id) throws WrappedResponse {
427
        DataFile datafile;
428
        if (id.equals(PERSISTENT_ID_KEY)) {
×
429
            String persistentId = getRequestParameter(PERSISTENT_ID_KEY.substring(1));
×
430
            if (persistentId == null) {
×
431
                throw new WrappedResponse(
×
432
                        badRequest(BundleUtil.getStringFromBundle("find.dataset.error.dataset_id_is_null", Collections.singletonList(PERSISTENT_ID_KEY.substring(1)))));
×
433
            }
434
            datafile = fileService.findByGlobalId(persistentId);
×
435
            if (datafile == null) {
×
436
                throw new WrappedResponse(notFound(BundleUtil.getStringFromBundle("find.datafile.error.dataset.not.found.persistentId", Collections.singletonList(persistentId))));
×
437
            }
438
            return datafile;
×
439
        } else {
440
            try {
441
                datafile = fileService.find(Long.parseLong(id));
×
442
                if (datafile == null) {
×
443
                    throw new WrappedResponse(notFound(BundleUtil.getStringFromBundle("find.datafile.error.datafile.not.found.id", Collections.singletonList(id))));
×
444
                }
445
                return datafile;
×
446
            } catch (NumberFormatException nfe) {
×
447
                throw new WrappedResponse(
×
448
                        badRequest(BundleUtil.getStringFromBundle("find.datafile.error.datafile.not.found.bad.id", Collections.singletonList(id))));
×
449
            }
450
        }
451
    }
452
       
453
    protected DataverseRole findRoleOrDie(String id) throws WrappedResponse {
454
        DataverseRole role;
455
        if (id.equals(ALIAS_KEY)) {
×
456
            String alias = getRequestParameter(ALIAS_KEY.substring(1));
×
457
            try {
458
                return em.createNamedQuery("DataverseRole.findDataverseRoleByAlias", DataverseRole.class)
×
459
                        .setParameter("alias", alias)
×
460
                        .getSingleResult();
×
461

462
            //Should not be a multiple result exception due to table constraint
463
            } catch (NoResultException nre) {
×
464
                throw new WrappedResponse(notFound(BundleUtil.getStringFromBundle("find.dataverse.role.error.role.not.found.alias", Collections.singletonList(alias))));
×
465
            }
466

467
        } else {
468

469
            try {
470
                role = rolesSvc.find(Long.parseLong(id));
×
471
                if (role == null) {
×
472
                    throw new WrappedResponse(notFound(BundleUtil.getStringFromBundle("find.dataverse.role.error.role.not.found.id", Collections.singletonList(id))));
×
473
                } else {
474
                    return role;
×
475
                }
476

477
            } catch (NumberFormatException nfe) {
×
478
                throw new WrappedResponse(
×
479
                        badRequest(BundleUtil.getStringFromBundle("find.dataverse.role.error.role.not.found.bad.id", Collections.singletonList(id))));
×
480
            }
481
        }
482
    }
483
    
484
    protected DatasetLinkingDataverse findDatasetLinkingDataverseOrDie(String datasetId, String linkingDataverseId) throws WrappedResponse {
485
        DatasetLinkingDataverse dsld;
486
        Dataverse linkingDataverse = findDataverseOrDie(linkingDataverseId);
×
487

488
        if (datasetId.equals(PERSISTENT_ID_KEY)) {
×
489
            String persistentId = getRequestParameter(PERSISTENT_ID_KEY.substring(1));
×
490
            if (persistentId == null) {
×
491
                throw new WrappedResponse(
×
492
                        badRequest(BundleUtil.getStringFromBundle("find.dataset.error.dataset_id_is_null", Collections.singletonList(PERSISTENT_ID_KEY.substring(1)))));
×
493
            }
494
            
495
            Dataset dataset = datasetSvc.findByGlobalId(persistentId);
×
496
            if (dataset == null) {
×
497
                throw new WrappedResponse(notFound(BundleUtil.getStringFromBundle("find.dataset.error.dataset.not.found.persistentId", Collections.singletonList(persistentId))));
×
498
            }
499
            datasetId = dataset.getId().toString();
×
500
        } 
501
        try {
502
            dsld = dsLinkingService.findDatasetLinkingDataverse(Long.parseLong(datasetId), linkingDataverse.getId());
×
503
            if (dsld == null) {
×
504
                throw new WrappedResponse(notFound(BundleUtil.getStringFromBundle("find.datasetlinking.error.not.found.ids", Arrays.asList(datasetId, linkingDataverse.getId().toString()))));
×
505
            }
506
            return dsld;
×
507
        } catch (NumberFormatException nfe) {
×
508
            throw new WrappedResponse(
×
509
                    badRequest(BundleUtil.getStringFromBundle("find.datasetlinking.error.not.found.bad.ids", Arrays.asList(datasetId, linkingDataverse.getId().toString()))));
×
510
        }
511
    }
512

513
    protected DataverseRequest createDataverseRequest( User u )  {
514
        return new DataverseRequest(u, httpRequest);
1✔
515
    }
516

517
        protected Dataverse findDataverse( String idtf ) {
518
                return isNumeric(idtf) ? dataverseSvc.find(Long.parseLong(idtf))
1✔
519
                                                                   : dataverseSvc.findByAlias(idtf);
1✔
520
        }
521

522
        protected DvObject findDvo( Long id ) {
523
                return em.createNamedQuery("DvObject.findById", DvObject.class)
×
524
                                .setParameter("id", id)
×
525
                                .getSingleResult();
×
526
        }
527

528
    /**
529
     * Tries to find a DvObject. If the passed id can be interpreted as a number,
530
     * it tries to get the DvObject by its id. Else, it tries to get a {@link Dataverse}
531
     * with that alias. If that fails, tries to get a {@link Dataset} with that global id.
532
     * @param id a value identifying the DvObject, either numeric of textual.
533
     * @return A DvObject, or {@code null}
534
     */
535
        protected DvObject findDvo( String id ) {
536
        if ( isNumeric(id) ) {
×
537
            return findDvo( Long.valueOf(id)) ;
×
538
        } else {
539
            Dataverse d = dataverseSvc.findByAlias(id);
×
540
            return ( d != null ) ?
×
541
                    d : datasetSvc.findByGlobalId(id);
×
542

543
        }
544
        }
545

546
    protected <T> T failIfNull( T t, String errorMessage ) throws WrappedResponse {
547
        if ( t != null ) return t;
1✔
548
        throw new WrappedResponse( error( Response.Status.BAD_REQUEST,errorMessage) );
×
549
    }
550

551
    protected MetadataBlock findMetadataBlock(Long id)  {
552
        return metadataBlockSvc.findById(id);
×
553
    }
554
    protected MetadataBlock findMetadataBlock(String idtf) throws NumberFormatException {
555
        return metadataBlockSvc.findByName(idtf);
1✔
556
    }
557

558
    protected DatasetFieldType findDatasetFieldType(String idtf) throws NumberFormatException {
559
        return isNumeric(idtf) ? datasetFieldSvc.find(Long.parseLong(idtf))
×
560
                : datasetFieldSvc.findByNameOpt(idtf);
×
561
    }
562

563
    /* =================== *\
564
     *  Command Execution  *
565
    \* =================== */
566

567
    /**
568
     * Executes a command, and returns the appropriate result/HTTP response.
569
     * @param <T> Return type for the command
570
     * @param cmd The command to execute.
571
     * @return Value from the command
572
     * @throws edu.harvard.iq.dataverse.api.AbstractApiBean.WrappedResponse Unwrap and return.
573
     * @see #response(java.util.concurrent.Callable)
574
     */
575
    protected <T> T execCommand( Command<T> cmd ) throws WrappedResponse {
576
        try {
577
            return engineSvc.submit(cmd);
1✔
578

NEW
579
        } catch (RateLimitCommandException ex) {
×
NEW
580
            throw new WrappedResponse(rateLimited(ex.getMessage()));
×
UNCOV
581
        } catch (IllegalCommandException ex) {
×
582
            //for 8859 for api calls that try to update datasets with TOA out of compliance
583
                if (ex.getMessage().toLowerCase().contains("terms of use")){
×
584
                    throw new WrappedResponse(ex, conflict(ex.getMessage()));
×
585
                }
586
            throw new WrappedResponse( ex, forbidden(ex.getMessage() ) );
×
587
        } catch (PermissionException ex) {
×
588
            /**
589
             * TODO Is there any harm in exposing ex.getLocalizedMessage()?
590
             * There's valuable information in there that can help people reason
591
             * about permissions! The formatting of the error would need to be
592
             * cleaned up but here's an example the helpful information:
593
             *
594
             * "User :guest is not permitted to perform requested action.Can't
595
             * execute command
596
             * edu.harvard.iq.dataverse.engine.command.impl.MoveDatasetCommand@50b150d9,
597
             * because request [DataverseRequest user:[GuestUser
598
             * :guest]@127.0.0.1] is missing permissions [AddDataset,
599
             * PublishDataset] on Object mra"
600
             *
601
             * Right now, the error that's visible via API (and via GUI
602
             * sometimes?) doesn't have much information in it:
603
             *
604
             * "User @jsmith is not permitted to perform requested action."
605
             */
606
            throw new WrappedResponse(error(Response.Status.UNAUTHORIZED,
×
607
                                                    "User " + cmd.getRequest().getUser().getIdentifier() + " is not permitted to perform requested action.") );
×
608

609
        } catch (CommandException ex) {
×
610
            Logger.getLogger(AbstractApiBean.class.getName()).log(Level.SEVERE, "Error while executing command " + cmd, ex);
×
611
            throw new WrappedResponse(ex, error(Status.INTERNAL_SERVER_ERROR, ex.getMessage()));
×
612
        }
613
    }
614

615
    /**
616
     * A syntactically nicer way of using {@link #execCommand(edu.harvard.iq.dataverse.engine.command.Command)}.
617
     * @param hdl The block to run.
618
     * @return HTTP Response appropriate for the way {@code hdl} executed.
619
     */
620
    protected Response response( Callable<Response> hdl ) {
621
        try {
622
            return hdl.call();
×
623
        } catch ( WrappedResponse rr ) {
×
624
            return rr.getResponse();
×
625
        } catch ( Exception ex ) {
×
626
            return handleDataverseRequestHandlerException(ex);
×
627
        }
628
    }
629

630
    /***
631
     * The preferred way of handling a request that requires a user. The method
632
     * receives a user and handles it to the handler for doing the actual work.
633
     *
634
     * @param hdl handling code block.
635
     * @param user the associated request user.
636
     * @return HTTP Response appropriate for the way {@code hdl} executed.
637
     */
638
    protected Response response(DataverseRequestHandler hdl, User user) {
639
        try {
640
            return hdl.handle(createDataverseRequest(user));
×
641
        } catch ( WrappedResponse rr ) {
×
642
            return rr.getResponse();
×
643
        } catch ( Exception ex ) {
×
644
            return handleDataverseRequestHandlerException(ex);
×
645
        }
646
    }
647

648
    private Response handleDataverseRequestHandlerException(Exception ex) {
649
        String incidentId = UUID.randomUUID().toString();
×
650
        logger.log(Level.SEVERE, "API internal error " + incidentId +": " + ex.getMessage(), ex);
×
651
        return Response.status(500)
×
652
                .entity(Json.createObjectBuilder()
×
653
                        .add("status", "ERROR")
×
654
                        .add("code", 500)
×
655
                        .add("message", "Internal server error. More details available at the server logs.")
×
656
                        .add("incidentId", incidentId)
×
657
                        .build())
×
658
                .type("application/json").build();
×
659
    }
660

661
    /* ====================== *\
662
     *  HTTP Response methods *
663
    \* ====================== */
664

665
    protected Response ok( JsonArrayBuilder bld ) {
666
        return Response.ok(Json.createObjectBuilder()
×
667
            .add("status", ApiConstants.STATUS_OK)
×
668
            .add("data", bld).build())
×
669
            .type(MediaType.APPLICATION_JSON).build();
×
670
    }
671

672
    protected Response ok( JsonArrayBuilder bld , long totalCount) {
673
        return Response.ok(Json.createObjectBuilder()
×
674
                        .add("status", ApiConstants.STATUS_OK)
×
675
                        .add("totalCount", totalCount)
×
676
                        .add("data", bld).build())
×
677
                .type(MediaType.APPLICATION_JSON).build();
×
678
    }
679

680
    protected Response ok( JsonArray ja ) {
681
        return Response.ok(Json.createObjectBuilder()
×
682
            .add("status", ApiConstants.STATUS_OK)
×
683
            .add("data", ja).build())
×
684
            .type(MediaType.APPLICATION_JSON).build();
×
685
    }
686

687
    protected Response ok( JsonObjectBuilder bld ) {
688
        return Response.ok( Json.createObjectBuilder()
×
689
            .add("status", ApiConstants.STATUS_OK)
×
690
            .add("data", bld).build() )
×
691
            .type(MediaType.APPLICATION_JSON)
×
692
            .build();
×
693
    }
694
    
695
    protected Response ok( JsonObject jo ) {
696
        return Response.ok( Json.createObjectBuilder()
×
697
                .add("status", ApiConstants.STATUS_OK)
×
698
                .add("data", jo).build() )
×
699
                .type(MediaType.APPLICATION_JSON)
×
700
                .build();    
×
701
    }
702

703
    protected Response ok( String msg ) {
704
        return Response.ok().entity(Json.createObjectBuilder()
1✔
705
            .add("status", ApiConstants.STATUS_OK)
1✔
706
            .add("data", Json.createObjectBuilder().add("message",msg)).build() )
1✔
707
            .type(MediaType.APPLICATION_JSON)
1✔
708
            .build();
1✔
709
    }
710
    
711
    protected Response ok( String msg, JsonObjectBuilder bld  ) {
712
        return Response.ok().entity(Json.createObjectBuilder()
×
713
            .add("status", ApiConstants.STATUS_OK)
×
714
            .add("message", Json.createObjectBuilder().add("message",msg))     
×
715
            .add("data", bld).build())      
×
716
            .type(MediaType.APPLICATION_JSON)
×
717
            .build();
×
718
    }
719

720
    protected Response ok( boolean value ) {
721
        return Response.ok().entity(Json.createObjectBuilder()
×
722
            .add("status", ApiConstants.STATUS_OK)
×
723
            .add("data", value).build() ).build();
×
724
    }
725

726
    protected Response ok(long value) {
727
        return Response.ok().entity(Json.createObjectBuilder()
×
728
                .add("status", ApiConstants.STATUS_OK)
×
729
                .add("data", value).build()).build();
×
730
    }
731

732
    /**
733
     * @param data Payload to return.
734
     * @param mediaType Non-JSON media type.
735
     * @param downloadFilename - add Content-Disposition header to suggest filename if not null
736
     * @return Non-JSON response, such as a shell script.
737
     */
738
    protected Response ok(String data, MediaType mediaType, String downloadFilename) {
739
        ResponseBuilder res =Response.ok().entity(data).type(mediaType);
×
740
        if(downloadFilename != null) {
×
741
            res = res.header("Content-Disposition", "attachment; filename=" + downloadFilename);
×
742
        }
743
        return res.build();
×
744
    }
745

746
    protected Response ok(InputStream inputStream) {
747
        ResponseBuilder res = Response.ok().entity(inputStream).type(MediaType.valueOf(FileUtil.MIME_TYPE_UNDETERMINED_DEFAULT));
×
748
        return res.build();
×
749
    }
750

751
    protected Response created( String uri, JsonObjectBuilder bld ) {
752
        return Response.created( URI.create(uri) )
×
753
                .entity( Json.createObjectBuilder()
×
754
                .add("status", "OK")
×
755
                .add("data", bld).build())
×
756
                .type(MediaType.APPLICATION_JSON)
×
757
                .build();
×
758
    }
759
    
760
    protected Response accepted(JsonObjectBuilder bld) {
761
        return Response.accepted()
×
762
                .entity(Json.createObjectBuilder()
×
763
                        .add("status", STATUS_WF_IN_PROGRESS)
×
764
                        .add("data",bld).build()
×
765
                ).build();
×
766
    }
767
    
768
    protected Response accepted() {
769
        return Response.accepted()
×
770
                .entity(Json.createObjectBuilder()
×
771
                        .add("status", STATUS_WF_IN_PROGRESS).build()
×
772
                ).build();
×
773
    }
774

775
    protected Response notFound( String msg ) {
776
        return error(Status.NOT_FOUND, msg);
×
777
    }
778

779
    protected Response badRequest( String msg ) {
780
        return error( Status.BAD_REQUEST, msg );
1✔
781
    }
782

783
    protected Response forbidden( String msg ) {
784
        return error( Status.FORBIDDEN, msg );
×
785
    }
786

787
    protected Response rateLimited( String msg ) {
NEW
788
        return error( Status.TOO_MANY_REQUESTS, msg );
×
789
    }
790

791
    protected Response conflict( String msg ) {
792
        return error( Status.CONFLICT, msg );
×
793
    }
794

795
    protected Response authenticatedUserRequired() {
796
        return error(Status.UNAUTHORIZED, RESPONSE_MESSAGE_AUTHENTICATED_USER_REQUIRED);
×
797
    }
798

799
    protected Response permissionError( PermissionException pe ) {
800
        return permissionError( pe.getMessage() );
×
801
    }
802

803
    protected Response permissionError( String message ) {
804
        return unauthorized( message );
×
805
    }
806
    
807
    protected Response unauthorized( String message ) {
808
        return error( Status.UNAUTHORIZED, message );
×
809
    }
810

811
    protected static Response error( Status sts, String msg ) {
812
        return Response.status(sts)
1✔
813
                .entity( NullSafeJsonBuilder.jsonObjectBuilder()
1✔
814
                        .add("status", ApiConstants.STATUS_ERROR)
1✔
815
                        .add( "message", msg ).build()
1✔
816
                ).type(MediaType.APPLICATION_JSON_TYPE).build();
1✔
817
    }
818
}
819

820
class LazyRef<T> {
821
    private interface Ref<T> {
822
        T get();
823
    }
824

825
    private Ref<T> ref;
826

827
    public LazyRef( final Callable<T> initer ) {
1✔
828
        ref = () -> {
1✔
829
            try {
830
                final T t = initer.call();
×
831
                ref = () -> t;
×
832
                return ref.get();
×
833
            } catch (Exception ex) {
×
834
                Logger.getLogger(LazyRef.class.getName()).log(Level.SEVERE, null, ex);
×
835
                return null;
×
836
            }
837
        };
838
    }
1✔
839

840
    public T get()  {
841
        return ref.get();
×
842
    }
843
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc