/**
*
*/
package com.thinkbiganalytics.metadata.modeshape;
/*-
* #%L
* thinkbig-metadata-modeshape
* %%
* Copyright (C) 2017 ThinkBig Analytics
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import com.thinkbiganalytics.metadata.api.MetadataAccess;
import com.thinkbiganalytics.metadata.api.MetadataAccessException;
import com.thinkbiganalytics.metadata.api.MetadataAction;
import com.thinkbiganalytics.metadata.api.MetadataCommand;
import com.thinkbiganalytics.metadata.api.MetadataExecutionException;
import com.thinkbiganalytics.metadata.api.MetadataRollbackAction;
import com.thinkbiganalytics.metadata.api.MetadataRollbackCommand;
import com.thinkbiganalytics.metadata.modeshape.security.ModeShapePrincipal;
import com.thinkbiganalytics.metadata.modeshape.security.ModeShapeReadOnlyPrincipal;
import com.thinkbiganalytics.metadata.modeshape.security.ModeShapeReadWritePrincipal;
import com.thinkbiganalytics.metadata.modeshape.security.OverrideCredentials;
import com.thinkbiganalytics.metadata.modeshape.security.SpringAuthenticationCredentials;
import com.thinkbiganalytics.metadata.modeshape.support.JcrUtil;
import com.thinkbiganalytics.metadata.modeshape.support.JcrVersionUtil;
import com.thinkbiganalytics.security.UsernamePrincipal;
import org.modeshape.jcr.api.txn.TransactionManagerLookup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import java.security.Principal;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.inject.Inject;
import javax.inject.Named;
import javax.jcr.Credentials;
import javax.jcr.Node;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.transaction.NotSupportedException;
import javax.transaction.SystemException;
import javax.transaction.TransactionManager;
/**
*
*/
public class JcrMetadataAccess implements MetadataAccess {
public static final String TBA_PREFIX = "tba";
/**
* Namespace for user-defined items
*/
public static final String USR_PREFIX = "usr";
private static final Logger log = LoggerFactory.getLogger(JcrMetadataAccess.class);
private static final ThreadLocal<ActiveSession> activeSession = new ThreadLocal<ActiveSession>() {
protected ActiveSession initialValue() {
return null;
}
};
private static final ThreadLocal<Set<Consumer<Boolean>>> postTransactionActions = new ThreadLocal<Set<Consumer<Boolean>>>() {
protected Set<Consumer<Boolean>> initialValue() {
return new HashSet<Consumer<Boolean>>();
}
};
private static final ThreadLocal<Set<Node>> checkedOutNodes = new ThreadLocal<Set<Node>>() {
protected java.util.Set<Node> initialValue() {
return new HashSet<>();
}
;
};
private static MetadataRollbackAction nullRollbackAction = (e) -> {
};
private static MetadataRollbackCommand nullRollbackCommand = (e) -> {
nullRollbackAction.execute(e);
};
@Inject
@Named("metadataJcrRepository")
private Repository repository;
@Inject
private TransactionManagerLookup txnLookup;
public static boolean hasActiveSession() {
return activeSession.get() != null;
}
public static Session getActiveSession() {
ActiveSession active = activeSession.get();
if (active != null) {
return active.session;
} else {
throw new NoActiveSessionException();
}
}
public static UsernamePrincipal getActiveUser() {
ActiveSession active = activeSession.get();
if (active != null) {
return active.userPrincipal;
} else {
throw new NoActiveSessionException();
}
}
/**
* Return all nodes that have been checked out
*/
public static Set<Node> getCheckedoutNodes() {
return checkedOutNodes.get();
}
/**
* Check out the node and add it to the Set of checked out nodes
*/
public static void ensureCheckoutNode(Node n) throws RepositoryException {
if (n.getSession().getRootNode().equals(n.getParent())) {
return;
} else if (JcrUtil.isVersionable(n) && (!n.isCheckedOut() || (n.isNew() && !checkedOutNodes.get().contains(n)))) {
JcrVersionUtil.checkout(n);
checkedOutNodes.get().add(n);
} else {
ensureCheckoutNode(n.getParent());
}
}
/**
* A set of Nodes that have been Checked Out. Nodes that have the mix:versionable need to be Checked Out and then Checked In when updating.
*
* @see com.thinkbiganalytics.metadata.modeshape.support.JcrPropertyUtil#setProperty(Node, String, Object) which checks out the node before applying the update
*/
public static void checkinNodes() throws RepositoryException {
Set<Node> checkedOutNodes = getCheckedoutNodes();
for (Iterator<Node> itr = checkedOutNodes.iterator(); itr.hasNext(); ) {
Node element = itr.next();
JcrVersionUtil.checkin(element);
itr.remove();
}
}
public static void addPostTransactionAction(Consumer<Boolean> action) {
postTransactionActions.get().add(action);
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.metadata.api.MetadataAccess#commit(com.thinkbiganalytics.metadata.api.MetadataCommand, java.security.Principal[])
*/
@Override
public <R> R commit(MetadataCommand<R> cmd, Principal... principals) {
return commit(createCredentials(false, principals), cmd);
}
public <R> R commit(MetadataCommand<R> cmd, MetadataRollbackCommand rollbackCmd, Principal... principals) {
return commit(createCredentials(false, principals), cmd, rollbackCmd);
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.metadata.api.MetadataAccess#commit(java.lang.Runnable, java.security.Principal[])
*/
public void commit(MetadataAction action, Principal... principals) {
commit(() -> {
action.execute();
return null;
}, principals);
}
public void commit(MetadataAction action, MetadataRollbackAction rollbackAction, Principal... principals) {
commit(() -> {
action.execute();
return null;
}, (e) -> {
rollbackAction.execute(e);
}, principals);
}
public void commit(Credentials creds, MetadataAction action) {
commit(creds, () -> {
action.execute();
return null;
});
}
public <R> R commit(Credentials creds, MetadataCommand<R> cmd) {
return commit(creds, cmd, nullRollbackCommand);
}
public <R> R commit(Credentials creds, MetadataCommand<R> cmd, MetadataRollbackCommand rollbackCmd) {
ActiveSession active = activeSession.get();
if (active == null) {
try {
activeSession.set(new ActiveSession(this.repository.login(creds)));
TransactionManager txnMgr = this.txnLookup.getTransactionManager();
try {
txnMgr.begin();
R result = execute(creds, cmd);
activeSession.get().session.save();
checkinNodes();
txnMgr.commit();
performPostTransactionActions(true);
return result;
} catch (Exception e) {
log.warn("Exception while execution a transactional operation - rolling back", e);
try {
txnMgr.rollback();
} catch (SystemException se) {
log.error("Failed to rollback transaction as a result of other transactional errors", se);
}
if (rollbackCmd != null) {
try {
rollbackCmd.execute(e);
} catch (Exception rbe) {
log.error("Failed to execution rollback command", rbe);
}
}
activeSession.get().session.refresh(false);
performPostTransactionActions(false);
throw e;
} finally {
activeSession.get().session.logout();
activeSession.remove();
postTransactionActions.remove();
checkedOutNodes.remove();
}
} catch (RuntimeException e) {
throw e;
} catch (RepositoryException e) {
throw new MetadataAccessException("Failure accessing the metadata store", e);
} catch (Exception e) {
throw new MetadataExecutionException(e);
}
} else {
try {
return cmd.execute();
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new MetadataExecutionException(e);
}
}
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.metadata.api.MetadataAccess#read(com.thinkbiganalytics.metadata.api.MetadataCommand, java.security.Principal[])
*/
@Override
public <R> R read(MetadataCommand<R> cmd, Principal... principals) {
return read(createCredentials(true, principals), cmd);
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.metadata.api.MetadataAccess#read(java.lang.Runnable, java.security.Principal[])
*/
public void read(MetadataAction action, Principal... principals) {
read(() -> {
action.execute();
return null;
}, principals);
}
public void read(Credentials creds, MetadataAction action) {
read(creds, () -> {
action.execute();
return null;
});
}
public <R> R read(Credentials creds, MetadataCommand<R> cmd) {
ActiveSession session = activeSession.get();
if (session == null) {
try {
activeSession.set(new ActiveSession(this.repository.login(creds)));
TransactionManager txnMgr = this.txnLookup.getTransactionManager();
try {
txnMgr.begin();
return execute(creds, cmd);
} finally {
try {
txnMgr.rollback();
} catch (SystemException e) {
log.error("Failed to rollback transaction", e);
}
activeSession.get().session.refresh(false);
activeSession.get().session.logout();
activeSession.remove();
}
} catch (SystemException | NotSupportedException | RepositoryException e) {
throw new MetadataAccessException("Failure accessing the metadata store", e);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new MetadataExecutionException(e);
}
} else {
try {
return cmd.execute();
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new MetadataExecutionException(e);
}
}
}
/**
* Execute the command in the context of the given credentials.
*/
private <R> R execute(Credentials creds, MetadataCommand<R> cmd) throws Exception {
if (creds instanceof OverrideCredentials) {
// If using override credentials first replace any existing Authentication (might be null) with
// the Authentication built from the overriding principals.
OverrideCredentials overrideCreds = (OverrideCredentials) creds;
Authentication initialAuth = SecurityContextHolder.getContext().getAuthentication();
SecurityContextHolder.getContext().setAuthentication(overrideCreds.getAuthentication());
try {
return cmd.execute();
} finally {
// Set the current Authentication back to what it was originally.
SecurityContextHolder.getContext().setAuthentication(initialAuth);
}
} else {
return cmd.execute();
}
}
/**
* Invokes all of the post-commit consumers; passing the transaction success flag to each.
*
* @param success true if the transaction committed successfully, otherwise false
*/
private void performPostTransactionActions(boolean success) {
postTransactionActions.get().stream().forEach(action -> action.accept(success));
}
private Credentials createCredentials(boolean readOnly, Principal... principals) {
Credentials creds = null;
// Using a default principal that is read-only or read-write since we will use ACLs when we implement entity-level access control.
ModeShapePrincipal defaultPrincipal = readOnly ? ModeShapeReadOnlyPrincipal.INSTANCE : ModeShapeReadWritePrincipal.INSTANCE;
if (principals.length == 0) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
creds = new SpringAuthenticationCredentials(auth, defaultPrincipal);
} else {
creds = OverrideCredentials.create(Stream.concat(Stream.of(defaultPrincipal),
Arrays.stream(principals))
.collect(Collectors.toSet()));
}
return creds;
}
private static class ActiveSession {
private final Session session;
private final UsernamePrincipal userPrincipal;
public ActiveSession(Session sess) {
this.session = sess;
this.userPrincipal = new UsernamePrincipal(sess.getUserID());
}
}
}