package edu.harvard.iq.dataverse;
import edu.harvard.iq.dataverse.actionlogging.ActionLogRecord;
import edu.harvard.iq.dataverse.actionlogging.ActionLogServiceBean;
import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean;
import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUserServiceBean;
import edu.harvard.iq.dataverse.engine.DataverseEngine;
import edu.harvard.iq.dataverse.authorization.Permission;
import edu.harvard.iq.dataverse.authorization.groups.impl.explicit.ExplicitGroupServiceBean;
import edu.harvard.iq.dataverse.engine.command.Command;
import edu.harvard.iq.dataverse.engine.command.CommandContext;
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.PermissionException;
import edu.harvard.iq.dataverse.ingest.IngestServiceBean;
import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean;
import edu.harvard.iq.dataverse.search.IndexServiceBean;
import edu.harvard.iq.dataverse.search.SearchServiceBean;
import java.util.Map;
import java.util.Set;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.inject.Named;
import edu.harvard.iq.dataverse.search.SolrIndexServiceBean;
import edu.harvard.iq.dataverse.search.savedsearch.SavedSearchServiceBean;
import edu.harvard.iq.dataverse.settings.SettingsServiceBean;
import edu.harvard.iq.dataverse.util.SystemConfig;
import java.util.EnumSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.EJBException;
import javax.ejb.TransactionAttribute;
import static javax.ejb.TransactionAttributeType.REQUIRES_NEW;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
/**
* An EJB capable of executing {@link Command}s in a JEE environment.
*
* @author michael
*/
@Stateless
@Named
public class EjbDataverseEngine {
private static final Logger logger = Logger.getLogger(EjbDataverseEngine.class.getCanonicalName());
@EJB
DatasetServiceBean datasetService;
@EJB
DataverseServiceBean dataverseService;
@EJB
DataverseRoleServiceBean roleService;
@EJB
DataverseRoleServiceBean rolesService;
@EJB
BuiltinUserServiceBean usersService;
@EJB
IndexServiceBean indexService;
@EJB
SolrIndexServiceBean solrIndexService;
@EJB
SearchServiceBean searchService;
@EJB
IngestServiceBean ingestService;
@EJB
PermissionServiceBean permissionService;
@EJB
DvObjectServiceBean dvObjectService;
@EJB
DataverseFacetServiceBean dataverseFacetService;
@EJB
FeaturedDataverseServiceBean featuredDataverseService;
@EJB
DataFileServiceBean dataFileService;
@EJB
TemplateServiceBean templateService;
@EJB
SavedSearchServiceBean savedSearchService;
@EJB
DataverseFieldTypeInputLevelServiceBean fieldTypeInputLevels;
@EJB
DOIEZIdServiceBean doiEZId;
@EJB
DOIDataCiteServiceBean doiDataCite;
@EJB
HandlenetServiceBean handleNet;
@EJB
SettingsServiceBean settings;
@EJB
GuestbookServiceBean guestbookService;
@EJB
GuestbookResponseServiceBean responses;
@EJB
DataverseLinkingServiceBean dvLinking;
@EJB
DatasetLinkingServiceBean dsLinking;
@EJB
ExplicitGroupServiceBean explicitGroups;
@EJB
RoleAssigneeServiceBean roleAssignees;
@EJB
UserNotificationServiceBean userNotificationService;
@EJB
AuthenticationServiceBean authentication;
@EJB
SystemConfig systemConfig;
@EJB
PrivateUrlServiceBean privateUrlService;
@PersistenceContext(unitName = "VDCNet-ejbPU")
private EntityManager em;
@EJB
ActionLogServiceBean logSvc;
private CommandContext ctxt;
@TransactionAttribute(REQUIRES_NEW)
public <R> R submitInNewTransaction(Command<R> aCommand) throws CommandException {
return submit(aCommand);
}
public <R> R submit(Command<R> aCommand) throws CommandException {
final ActionLogRecord logRec = new ActionLogRecord(ActionLogRecord.ActionType.Command, aCommand.getClass().getCanonicalName());
try {
logRec.setUserIdentifier( aCommand.getRequest().getUser().getIdentifier() );
// Check permissions - or throw an exception
Map<String, ? extends Set<Permission>> requiredMap = aCommand.getRequiredPermissions();
if (requiredMap == null) {
throw new RuntimeException("Command " + aCommand + " does not define required permissions.");
}
DataverseRequest dvReq = aCommand.getRequest();
Map<String, DvObject> affectedDvObjects = aCommand.getAffectedDvObjects();
logRec.setInfo( describe(affectedDvObjects) );
for (Map.Entry<String, ? extends Set<Permission>> pair : requiredMap.entrySet()) {
String dvName = pair.getKey();
if (!affectedDvObjects.containsKey(dvName)) {
throw new RuntimeException("Command instance " + aCommand + " does not have a DvObject named '" + dvName + "'");
}
DvObject dvo = affectedDvObjects.get(dvName);
Set<Permission> granted = (dvo != null) ? permissionService.permissionsFor(dvReq, dvo)
: EnumSet.allOf(Permission.class);
Set<Permission> required = requiredMap.get(dvName);
if (!granted.containsAll(required)) {
required.removeAll(granted);
logRec.setActionResult(ActionLogRecord.Result.PermissionError);
/**
* @todo Is there any harm in showing the "granted" set
* since we already show "required"? It would help people
* reason about the mismatch.
*/
throw new PermissionException("Can't execute command " + aCommand
+ ", because request " + aCommand.getRequest()
+ " is missing permissions " + required
+ " on Object " + dvo.accept(DvObject.NamePrinter),
aCommand,
required, dvo);
}
}
try {
return aCommand.execute(getContext());
} catch ( EJBException ejbe ) {
logRec.setActionResult(ActionLogRecord.Result.InternalError);
throw new CommandException("Command " + aCommand.toString() + " failed: " + ejbe.getMessage(), ejbe.getCausedByException(), aCommand);
}
} catch ( RuntimeException re ) {
logRec.setActionResult(ActionLogRecord.Result.InternalError);
logRec.setInfo( re.getMessage() );
Throwable cause = re;
while (cause != null) {
if (cause instanceof ConstraintViolationException) {
StringBuilder sb = new StringBuilder();
sb.append("Unexpected bean validation constraint exception:");
ConstraintViolationException constraintViolationException = (ConstraintViolationException) cause;
for (ConstraintViolation<?> violation : constraintViolationException.getConstraintViolations()) {
sb.append(" Invalid value: <<<").append(violation.getInvalidValue()).append(">>> for ").append(violation.getPropertyPath()).append(" at ").append(violation.getLeafBean()).append(" - ").append(violation.getMessage());
}
logger.log(Level.SEVERE, sb.toString());
// set this more detailed info in action log
logRec.setInfo( sb.toString() );
}
cause = cause.getCause();
}
throw re;
} finally {
if ( logRec.getActionResult() == null ) {
logRec.setActionResult( ActionLogRecord.Result.OK );
}
logRec.setEndTime( new java.util.Date() );
logSvc.log(logRec);
}
}
public CommandContext getContext() {
if (ctxt == null) {
ctxt = new CommandContext() {
@Override
public DatasetServiceBean datasets() {
return datasetService;
}
@Override
public DataverseServiceBean dataverses() {
return dataverseService;
}
@Override
public DataverseRoleServiceBean roles() {
return rolesService;
}
@Override
public BuiltinUserServiceBean builtinUsers() {
return usersService;
}
@Override
public IndexServiceBean index() {
return indexService;
}
@Override
public SolrIndexServiceBean solrIndex() {
return solrIndexService;
}
@Override
public SearchServiceBean search() {
return searchService;
}
@Override
public IngestServiceBean ingest() {
return ingestService;
}
@Override
public PermissionServiceBean permissions() {
return permissionService;
}
@Override
public DvObjectServiceBean dvObjects() {
return dvObjectService;
}
@Override
public DataFileServiceBean files() {
return dataFileService;
}
@Override
public EntityManager em() {
return em;
}
@Override
public DataverseFacetServiceBean facets() {
return dataverseFacetService;
}
@Override
public FeaturedDataverseServiceBean featuredDataverses() {
return featuredDataverseService;
}
@Override
public TemplateServiceBean templates() {
return templateService;
}
@Override
public SavedSearchServiceBean savedSearches() {
return savedSearchService;
}
@Override
public DataverseFieldTypeInputLevelServiceBean fieldTypeInputLevels() {
return fieldTypeInputLevels;
}
@Override
public DOIEZIdServiceBean doiEZId() {
return doiEZId;
}
@Override
public DOIDataCiteServiceBean doiDataCite() {
return doiDataCite;
}
@Override
public HandlenetServiceBean handleNet() {
return handleNet;
}
@Override
public SettingsServiceBean settings() {
return settings;
}
@Override
public GuestbookServiceBean guestbooks() {
return guestbookService;
}
@Override
public GuestbookResponseServiceBean responses() {
return responses;
}
@Override
public DataverseLinkingServiceBean dvLinking() {
return dvLinking;
}
@Override
public DatasetLinkingServiceBean dsLinking() {
return dsLinking;
}
@Override
public DataverseEngine engine() {
return new DataverseEngine() {
@Override
public <R> R submit(Command<R> aCommand) throws CommandException {
return EjbDataverseEngine.this.submit(aCommand);
}
};
}
@Override
public ExplicitGroupServiceBean explicitGroups() {
return explicitGroups;
}
@Override
public RoleAssigneeServiceBean roleAssignees() {
return roleAssignees;
}
@Override
public UserNotificationServiceBean notifications() {
return userNotificationService;
}
@Override
public AuthenticationServiceBean authentication() {
return authentication;
}
@Override
public SystemConfig systemConfig() {
return systemConfig;
}
@Override
public PrivateUrlServiceBean privateUrl() {
return privateUrlService;
}
};
}
return ctxt;
}
private String describe( Map<String, DvObject> dvObjMap ) {
StringBuilder sb = new StringBuilder();
for ( Map.Entry<String, DvObject> ent : dvObjMap.entrySet() ) {
DvObject value = ent.getValue();
sb.append(ent.getKey()).append(":");
sb.append( (value!=null) ? value.accept(DvObject.NameIdPrinter) : "<null>");
sb.append(" ");
}
return sb.toString();
}
}