package edu.harvard.iq.dataverse.api; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetFieldServiceBean; import edu.harvard.iq.dataverse.DatasetFieldType; import edu.harvard.iq.dataverse.DatasetServiceBean; import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.DataverseRoleServiceBean; import edu.harvard.iq.dataverse.DataverseServiceBean; import edu.harvard.iq.dataverse.DvObject; import edu.harvard.iq.dataverse.EjbDataverseEngine; import edu.harvard.iq.dataverse.MetadataBlock; import edu.harvard.iq.dataverse.MetadataBlockServiceBean; import edu.harvard.iq.dataverse.PermissionServiceBean; import edu.harvard.iq.dataverse.RoleAssigneeServiceBean; import edu.harvard.iq.dataverse.UserNotificationServiceBean; import edu.harvard.iq.dataverse.UserServiceBean; import edu.harvard.iq.dataverse.actionlogging.ActionLogServiceBean; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.RoleAssignee; import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailServiceBean; import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; import edu.harvard.iq.dataverse.engine.command.exception.PermissionException; import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean; import edu.harvard.iq.dataverse.search.savedsearch.SavedSearchServiceBean; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.json.JsonParser; import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; import edu.harvard.iq.dataverse.validation.BeanValidationServiceBean; import java.io.StringReader; import java.net.URI; import java.util.UUID; import java.util.concurrent.Callable; import java.util.logging.Level; import java.util.logging.Logger; import javax.ejb.EJB; import javax.ejb.EJBException; import javax.json.Json; import javax.json.JsonArrayBuilder; import javax.json.JsonObject; import javax.json.JsonObjectBuilder; import javax.json.JsonReader; import javax.json.JsonValue; import javax.json.JsonValue.ValueType; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; /** * Base class for API beans * @author michael */ public abstract class AbstractApiBean { private static final Logger logger = Logger.getLogger(AbstractApiBean.class.getName()); private static final String DATAVERSE_KEY_HEADER_NAME = "X-Dataverse-key"; /** * Utility class to convey a proper error response using Java's exceptions. */ public static class WrappedResponse extends Exception { private final Response response; public WrappedResponse(Response response) { this.response = response; } public WrappedResponse( Throwable cause, Response response ) { super( cause ); this.response = response; } public Response getResponse() { return response; } /** * Creates a new response, based on the original response and the passed message. * Typical use would be to add a better error message to the HTTP response. * @param message additional message to be added to the response. * @return A Response with updated message field. */ public Response refineResponse( String message ) { final Status statusCode = Response.Status.fromStatusCode(response.getStatus()); String baseMessage = getWrappedMessageWhenJson(); if ( baseMessage == null ) { final Throwable cause = getCause(); baseMessage = (cause!=null ? cause.getMessage() : ""); } return error(statusCode, message+" "+baseMessage); } /** * In the common case of the wrapped response being of type JSON, * return the message field it has (if any). * @return the content of a message field, or {@code null}. */ String getWrappedMessageWhenJson() { if ( response.getMediaType().equals(MediaType.APPLICATION_JSON_TYPE) ) { Object entity = response.getEntity(); if ( entity == null ) return null; String json = entity.toString(); try ( StringReader rdr = new StringReader(json) ){ JsonReader jrdr = Json.createReader(rdr); JsonObject obj = jrdr.readObject(); if ( obj.containsKey("message") ) { JsonValue message = obj.get("message"); return message.getValueType() == ValueType.STRING ? obj.getString("message") : message.toString(); } else { return null; } } } else { return null; } } } @EJB protected EjbDataverseEngine engineSvc; @EJB protected DatasetServiceBean datasetSvc; @EJB protected DataverseServiceBean dataverseSvc; @EJB protected AuthenticationServiceBean authSvc; @EJB protected DatasetFieldServiceBean datasetFieldSvc; @EJB protected MetadataBlockServiceBean metadataBlockSvc; @EJB protected UserServiceBean userSvc; @EJB protected DataverseRoleServiceBean rolesSvc; @EJB protected SettingsServiceBean settingsSvc; @EJB protected RoleAssigneeServiceBean roleAssigneeSvc; @EJB protected PermissionServiceBean permissionSvc; @EJB protected GroupServiceBean groupSvc; @EJB protected ActionLogServiceBean actionLogSvc; @EJB protected BeanValidationServiceBean beanValidationSvc; @EJB protected SavedSearchServiceBean savedSearchSvc; @EJB protected PrivateUrlServiceBean privateUrlSvc; @EJB protected ConfirmEmailServiceBean confirmEmailSvc; @EJB protected UserNotificationServiceBean userNotificationSvc; @PersistenceContext(unitName = "VDCNet-ejbPU") protected EntityManager em; @Context protected HttpServletRequest httpRequest; /** * For pretty printing (indenting) of JSON output. */ public enum Format { PRETTY } private final LazyRef<JsonParser> jsonParserRef = new LazyRef<>(new Callable<JsonParser>() { @Override public JsonParser call() throws Exception { return new JsonParser(datasetFieldSvc, metadataBlockSvc,settingsSvc); } }); /** * Functional interface for handling HTTP requests in the APIs. * * @see #response(edu.harvard.iq.dataverse.api.AbstractApiBean.DataverseRequestHandler) */ protected static interface DataverseRequestHandler { Response handle( DataverseRequest u ) throws WrappedResponse; } /* ===================== *\ * Utility Methods * * Get that DSL feelin' * \* ===================== */ protected JsonParser jsonParser() { return jsonParserRef.get(); } protected boolean isNumeric( String str ) { return Util.isNumeric(str); } protected boolean parseBooleanOrDie( String input ) throws WrappedResponse { if (input == null ) throw new WrappedResponse( badRequest("Boolean value missing")); input = input.trim(); if ( Util.isBoolean(input) ) { return Util.isTrue(input); } else { throw new WrappedResponse( badRequest("Illegal boolean value '" + input + "'")); } } /** * Returns the {@code key} query parameter from the current request, or {@code null} if * the request has no such parameter. * @param key Name of the requested parameter. * @return Value of the requested parameter in the current request. */ protected String getRequestParameter( String key ) { return httpRequest.getParameter(key); } protected String getRequestApiKey() { String headerParamApiKey = httpRequest.getHeader(DATAVERSE_KEY_HEADER_NAME); String queryParamApiKey = httpRequest.getParameter("key"); return headerParamApiKey!=null ? headerParamApiKey : queryParamApiKey; } /* ========= *\ * Finders * \* ========= */ protected RoleAssignee findAssignee(String identifier) { try { RoleAssignee roleAssignee = roleAssigneeSvc.getRoleAssignee(identifier); return roleAssignee; } catch (EJBException ex) { Throwable cause = ex; while (cause.getCause() != null) { cause = cause.getCause(); } logger.log(Level.INFO, "Exception caught looking up RoleAssignee based on identifier ''{0}'': {1}", new Object[]{identifier, cause.getMessage()}); return null; } } /** * * @param apiKey the key to find the user with * @return the user, or null * @see #findUserOrDie(java.lang.String) */ protected AuthenticatedUser findUserByApiToken( String apiKey ) { return authSvc.lookupUser(apiKey); } /** * Returns the user of pointed by the API key, or the guest user * @return a user, may be a guest user. * @throws edu.harvard.iq.dataverse.api.AbstractApiBean.WrappedResponse iff there is an api key present, but it is invalid. */ protected User findUserOrDie() throws WrappedResponse { final String requestApiKey = getRequestApiKey(); if (requestApiKey == null) { return GuestUser.get(); } PrivateUrlUser privateUrlUser = privateUrlSvc.getPrivateUrlUserFromToken(requestApiKey); if (privateUrlUser != null) { return privateUrlUser; } return findAuthenticatedUserOrDie(requestApiKey); } /** * Finds the authenticated user, based on (in order): * <ol> * <li>The key in the HTTP header {@link #DATAVERSE_KEY_HEADER_NAME}</li> * <li>The key in the query parameter {@code key} * </ol> * * If no user is found, throws a wrapped bad api key (HTTP UNAUTHORIZED) response. * * @return The authenticated user which owns the passed api key * @throws edu.harvard.iq.dataverse.api.AbstractApiBean.WrappedResponse in case said user is not found. */ protected AuthenticatedUser findAuthenticatedUserOrDie() throws WrappedResponse { return findAuthenticatedUserOrDie(getRequestApiKey()); } private AuthenticatedUser findAuthenticatedUserOrDie( String key ) throws WrappedResponse { AuthenticatedUser u = authSvc.lookupUser(key); if ( u != null ) { return u; } throw new WrappedResponse( badApiKey(key) ); } protected Dataverse findDataverseOrDie( String dvIdtf ) throws WrappedResponse { Dataverse dv = findDataverse(dvIdtf); if ( dv == null ) { throw new WrappedResponse(error( Response.Status.NOT_FOUND, "Can't find dataverse with identifier='" + dvIdtf + "'")); } return dv; } protected DataverseRequest createDataverseRequest( User u ) { return new DataverseRequest(u, httpRequest); } protected Dataverse findDataverse( String idtf ) { return isNumeric(idtf) ? dataverseSvc.find(Long.parseLong(idtf)) : dataverseSvc.findByAlias(idtf); } protected DvObject findDvo( Long id ) { return em.createNamedQuery("DvObject.findById", DvObject.class) .setParameter("id", id) .getSingleResult(); } /** * Tries to find a DvObject. If the passed id can be interpreted as a number, * it tries to get the DvObject by its id. Else, it tries to get a {@link Dataverse} * with that alias. If that fails, tries to get a {@link Dataset} with that global id. * @param id a value identifying the DvObject, either numeric of textual. * @return A DvObject, or {@code null} */ protected DvObject findDvo( String id ) { if ( isNumeric(id) ) { return findDvo( Long.valueOf(id)) ; } else { Dataverse d = dataverseSvc.findByAlias(id); return ( d != null ) ? d : datasetSvc.findByGlobalId(id); } } protected <T> T failIfNull( T t, String errorMessage ) throws WrappedResponse { if ( t != null ) return t; throw new WrappedResponse( error( Response.Status.BAD_REQUEST,errorMessage) ); } protected MetadataBlock findMetadataBlock(Long id) { return metadataBlockSvc.findById(id); } protected MetadataBlock findMetadataBlock(String idtf) throws NumberFormatException { return metadataBlockSvc.findByName(idtf); } protected DatasetFieldType findDatasetFieldType(String idtf) throws NumberFormatException { return isNumeric(idtf) ? datasetFieldSvc.find(Long.parseLong(idtf)) : datasetFieldSvc.findByNameOpt(idtf); } /* =================== *\ * Command Execution * \* =================== */ /** * Executes a command, and returns the appropriate result/HTTP response. * @param <T> Return type for the command * @param cmd The command to execute. * @return Value from the command * @throws edu.harvard.iq.dataverse.api.AbstractApiBean.WrappedResponse Unwrap and return. * @see #response(java.util.concurrent.Callable) */ protected <T> T execCommand( Command<T> cmd ) throws WrappedResponse { try { return engineSvc.submit(cmd); } catch (IllegalCommandException ex) { throw new WrappedResponse( ex, error(Response.Status.FORBIDDEN, ex.getMessage() ) ); } catch (PermissionException ex) { /** * @todo Is there any harm in exposing ex.getLocalizedMessage()? * There's valuable information in there that can help people reason * about permissions! */ throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, "User " + cmd.getRequest().getUser().getIdentifier() + " is not permitted to perform requested action.") ); } catch (CommandException ex) { Logger.getLogger(AbstractApiBean.class.getName()).log(Level.SEVERE, "Error while executing command " + cmd, ex); throw new WrappedResponse(ex, error(Status.INTERNAL_SERVER_ERROR, ex.getMessage())); } } /** * A syntactically nicer way of using {@link #execCommand(edu.harvard.iq.dataverse.engine.command.Command)}. * @param hdl The block to run. * @return HTTP Response appropriate for the way {@code hdl} executed. */ protected Response response( Callable<Response> hdl ) { try { return hdl.call(); } catch ( WrappedResponse rr ) { return rr.getResponse(); } catch ( Exception ex ) { String incidentId = UUID.randomUUID().toString(); logger.log(Level.SEVERE, "API internal error " + incidentId +": " + ex.getMessage(), ex); return Response.status(500) .entity( Json.createObjectBuilder() .add("status", "ERROR") .add("code", 500) .add("message", "Internal server error. More details available at the server logs.") .add("incidentId", incidentId) .build()) .type("application/json").build(); } } /** * The preferred way of handling a request that requires a user. The system * looks for the user and, if found, handles it to the handler for doing the * actual work. * * This is a relatively secure way to handle things, since if the user is not * found, the response is about the bad API key, rather than something else * (say, 404 NOT FOUND which leaks information about the existence of the * sought object). * * @param hdl handling code block. * @return HTTP Response appropriate for the way {@code hdl} executed. */ protected Response response( DataverseRequestHandler hdl ) { try { return hdl.handle(createDataverseRequest(findUserOrDie())); } catch ( WrappedResponse rr ) { return rr.getResponse(); } catch ( Exception ex ) { String incidentId = UUID.randomUUID().toString(); logger.log(Level.SEVERE, "API internal error " + incidentId +": " + ex.getMessage(), ex); return Response.status(500) .entity( Json.createObjectBuilder() .add("status", "ERROR") .add("code", 500) .add("message", "Internal server error. More details available at the server logs.") .add("incidentId", incidentId) .build()) .type("application/json").build(); } } /* ====================== *\ * HTTP Response methods * \* ====================== */ protected Response ok( JsonArrayBuilder bld ) { return Response.ok(Json.createObjectBuilder() .add("status", "OK") .add("data", bld).build()).build(); } protected Response ok( JsonObjectBuilder bld ) { return Response.ok( Json.createObjectBuilder() .add("status", "OK") .add("data", bld).build() ) .type(MediaType.APPLICATION_JSON) .build(); } protected Response ok( String msg ) { return Response.ok().entity(Json.createObjectBuilder() .add("status", "OK") .add("data", Json.createObjectBuilder().add("message",msg)).build() ) .type(MediaType.APPLICATION_JSON) .build(); } protected Response ok( boolean value ) { return Response.ok().entity(Json.createObjectBuilder() .add("status", "OK") .add("data", value).build() ).build(); } protected Response created( String uri, JsonObjectBuilder bld ) { return Response.created( URI.create(uri) ) .entity( Json.createObjectBuilder() .add("status", "OK") .add("data", bld).build()) .type(MediaType.APPLICATION_JSON) .build(); } protected Response accepted() { return Response.accepted() .entity(Json.createObjectBuilder() .add("status", "OK").build() ).build(); } protected Response notFound( String msg ) { return error(Status.NOT_FOUND, msg); } protected Response badRequest( String msg ) { return error( Status.BAD_REQUEST, msg ); } protected Response badApiKey( String apiKey ) { return error(Status.UNAUTHORIZED, (apiKey != null ) ? "Bad api key '" + apiKey +"'" : "Please provide a key query parameter (?key=XXX) or via the HTTP header " + DATAVERSE_KEY_HEADER_NAME ); } protected Response permissionError( PermissionException pe ) { return permissionError( pe.getMessage() ); } protected Response permissionError( String message ) { return error( Status.UNAUTHORIZED, message ); } protected static Response error( Status sts, String msg ) { return Response.status(sts) .entity( NullSafeJsonBuilder.jsonObjectBuilder() .add("status", "ERROR") .add( "message", msg ).build() ).type(MediaType.APPLICATION_JSON_TYPE).build(); } } class LazyRef<T> { private interface Ref<T> { T get(); } private Ref<T> ref; public LazyRef( final Callable<T> initer ) { ref = new Ref<T>(){ @Override public T get() { try { final T t = initer.call(); ref = new Ref<T>(){ @Override public T get() { return t;} }; return ref.get(); } catch (Exception ex) { Logger.getLogger(LazyRef.class.getName()).log(Level.SEVERE, null, ex); return null; } }}; } public T get() { return ref.get(); } }