package com.thinkbiganalytics.metadata.modeshape.datasource;
/*-
* #%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.MetadataException;
import com.thinkbiganalytics.metadata.api.datasource.Datasource;
import com.thinkbiganalytics.metadata.api.datasource.DatasourceCriteria;
import com.thinkbiganalytics.metadata.api.datasource.DatasourceDetails;
import com.thinkbiganalytics.metadata.api.datasource.DatasourceProvider;
import com.thinkbiganalytics.metadata.api.datasource.DerivedDatasource;
import com.thinkbiganalytics.metadata.api.datasource.UserDatasource;
import com.thinkbiganalytics.metadata.core.AbstractMetadataCriteria;
import com.thinkbiganalytics.metadata.modeshape.BaseJcrProvider;
import com.thinkbiganalytics.metadata.modeshape.JcrMetadataAccess;
import com.thinkbiganalytics.metadata.modeshape.MetadataRepositoryException;
import com.thinkbiganalytics.metadata.modeshape.common.EntityUtil;
import com.thinkbiganalytics.metadata.modeshape.common.JcrEntity;
import com.thinkbiganalytics.metadata.modeshape.security.action.JcrAllowedActions;
import com.thinkbiganalytics.metadata.modeshape.security.action.JcrAllowedEntityActionsProvider;
import com.thinkbiganalytics.metadata.modeshape.support.JcrObjectTypeResolver;
import com.thinkbiganalytics.metadata.modeshape.support.JcrQueryUtil;
import com.thinkbiganalytics.metadata.modeshape.support.JcrTool;
import com.thinkbiganalytics.metadata.modeshape.support.JcrUtil;
import com.thinkbiganalytics.security.AccessController;
import com.thinkbiganalytics.security.UsernamePrincipal;
import com.thinkbiganalytics.security.action.AllowedActions;
import com.thinkbiganalytics.security.role.SecurityRole;
import com.thinkbiganalytics.security.role.SecurityRoleProvider;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.joda.time.DateTime;
import org.modeshape.common.text.Jsr283Encoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.security.Principal;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.inject.Inject;
import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
/**
*/
public class JcrDatasourceProvider extends BaseJcrProvider<Datasource, Datasource.ID> implements DatasourceProvider {
private static final Logger log = LoggerFactory.getLogger(JcrDatasourceProvider.class);
private static final Map<Class<? extends Datasource>, Class<? extends JcrDatasource>> DOMAIN_TYPES_MAP;
private static final Map<String, Class<? extends JcrDatasource>> NODE_TYPES_MAP;
public static JcrObjectTypeResolver<? extends JcrDatasource> TYPE_RESOLVER = new JcrObjectTypeResolver<JcrDatasource>() {
@Override
public Class<? extends JcrDatasource> resolve(Node node) {
try {
if (NODE_TYPES_MAP.containsKey(node.getPrimaryNodeType().getName())) {
return NODE_TYPES_MAP.get(node.getPrimaryNodeType().getName());
} else {
return JcrDatasource.class;
}
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to determine type of node: " + node, e);
}
}
};
static {
Map<Class<? extends Datasource>, Class<? extends JcrDatasource>> map = new HashMap<>();
map.put(DerivedDatasource.class, JcrDerivedDatasource.class);
map.put(UserDatasource.class, JcrUserDatasource.class);
DOMAIN_TYPES_MAP = map;
}
static {
Map<String, Class<? extends JcrDatasource>> map = new HashMap<>();
map.put(JcrDerivedDatasource.NODE_TYPE, JcrDerivedDatasource.class);
map.put(JcrUserDatasource.NODE_TYPE, JcrUserDatasource.class);
NODE_TYPES_MAP = map;
}
@Inject
private JcrAllowedEntityActionsProvider actionsProvider;
@Inject
private SecurityRoleProvider roleProvider;
@Inject
private AccessController accessController;
public static Class<? extends JcrEntity> resolveJcrEntityClass(String jcrNodeType) {
if (NODE_TYPES_MAP.containsKey(jcrNodeType)) {
return NODE_TYPES_MAP.get(jcrNodeType);
} else {
return JcrDatasource.class;
}
}
public static Class<? extends JcrEntity> resolveJcrEntityClass(Node node) {
try {
return resolveJcrEntityClass(node.getPrimaryNodeType().getName());
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to determine type of node: " + node, e);
}
}
/**
* Finds the derived ds by Type and System Name
*/
public DerivedDatasource findDerivedDatasource(String datasourceType, String systemName) {
String query = "SELECT * from " + EntityUtil.asQueryProperty(JcrDerivedDatasource.NODE_TYPE) + " as e "
+ "WHERE e." + EntityUtil.asQueryProperty(JcrDerivedDatasource.TYPE_NAME) + " = $datasourceType "
+ "AND e." + EntityUtil.asQueryProperty(JcrDatasource.SYSTEM_NAME) + " = $identityString";
Map<String, String> bindParams = new HashMap<>();
bindParams.put("datasourceType", datasourceType);
bindParams.put("identityString", systemName);
return JcrQueryUtil.findFirst(getSession(), query, bindParams, JcrDerivedDatasource.class);
}
private JcrDerivedDatasource findDerivedDatasourceByNodeName(String nodeName) throws RepositoryException {
Node parentNode = getSession().getNode(EntityUtil.pathForDerivedDatasource());
try {
Node child = parentNode.getNode(nodeName);
if (child != null) {
JcrDerivedDatasource jcrDerivedDatasource = new JcrDerivedDatasource(child);
return jcrDerivedDatasource;
}
} catch (PathNotFoundException e) {
//this is ok if we cant find it we will try to create it.
}
return null;
}
/**
* gets or creates the Derived datasource
*/
public DerivedDatasource ensureDerivedDatasource(String datasourceType, String identityString, String title, String desc, Map<String, Object> properties) {
//ensure the identity String is not null
if (identityString == null) {
identityString = "";
}
if (datasourceType == null) {
datasourceType = "Datasource";
}
DerivedDatasource derivedDatasource = findDerivedDatasource(datasourceType, identityString);
if (derivedDatasource == null) {
try {
if (!getSession().getRootNode().hasNode("metadata/datasources/derived")) {
if (!getSession().getRootNode().hasNode("metadata/datasources")) {
getSession().getRootNode().addNode("metadata", "datasources");
}
getSession().getRootNode().getNode("metadata/datasources").addNode("derived");
}
Node parentNode = getSession().getNode(EntityUtil.pathForDerivedDatasource());
String nodeName = datasourceType + "-" + identityString;
if (Jsr283Encoder.containsEncodeableCharacters(identityString)) {
nodeName = new Jsr283Encoder().encode(nodeName);
}
JcrDerivedDatasource jcrDerivedDatasource = null;
try {
jcrDerivedDatasource = findDerivedDatasourceByNodeName(nodeName);
} catch (RepositoryException e) {
log.warn("An exception ocurred trying to find the DerivedDatasource by node name {}. {} ", nodeName, e.getMessage());
}
derivedDatasource = jcrDerivedDatasource;
if (jcrDerivedDatasource == null) {
Node derivedDatasourceNode = JcrUtil.createNode(parentNode, nodeName, JcrDerivedDatasource.NODE_TYPE);
jcrDerivedDatasource = new JcrDerivedDatasource(derivedDatasourceNode);
jcrDerivedDatasource.setSystemName(identityString);
jcrDerivedDatasource.setDatasourceType(datasourceType);
jcrDerivedDatasource.setTitle(title);
jcrDerivedDatasource.setDescription(desc);
derivedDatasource = jcrDerivedDatasource;
}
} catch (RepositoryException e) {
log.error("Failed to create Derived Datasource for DatasourceType: {}, IdentityString: {}, Error: {}", datasourceType, identityString, e.getMessage(), e);
}
}
if (derivedDatasource != null) {
// ((JcrDerivedDatasource)derivedDatasource).mergeProperties()
if (properties != null) {
derivedDatasource.setProperties(properties);
}
derivedDatasource.setTitle(title);
}
return derivedDatasource;
}
@Override
public Class<? extends Datasource> getEntityClass() {
return JcrDatasource.class;
}
@Override
public Class<? extends JcrEntity> getJcrEntityClass() {
return JcrDatasource.class;
}
@Override
public Class<? extends JcrEntity> getJcrEntityClass(String jcrNodeType) {
return resolveJcrEntityClass(jcrNodeType);
}
@Override
public String getNodeType(Class<? extends JcrEntity> jcrEntityType) {
try {
Field folderField = FieldUtils.getField(jcrEntityType, "NODE_TYPE", true);
String jcrType = (String) folderField.get(null);
return jcrType;
} catch (IllegalArgumentException | IllegalAccessException e) {
// Shouldn't really happen.
throw new MetadataException("Unable to determine JCR node the for entity class: " + jcrEntityType, e);
}
}
@Override
public DatasourceCriteria datasetCriteria() {
return new Criteria();
}
@Override
@SuppressWarnings("unchecked")
public <D extends Datasource> D ensureDatasource(String name, String descr, Class<D> type) {
JcrDatasource datasource = createImpl(name, descr, type);
datasource.setDescription(descr);
return (D) datasource;
}
@Override
public Datasource getDatasource(Datasource.ID id) {
return findById(id);
}
@Override
public List<Datasource> getDatasources() {
return findAll();
}
@Override
public List<Datasource> getDatasources(DatasourceCriteria criteria) {
return findAll().stream().filter((Criteria) criteria).collect(Collectors.toList());
}
@Override
public Datasource.ID resolve(Serializable id) {
return resolveId(id);
}
@Override
public void removeDatasource(Datasource.ID id) {
Datasource ds = getDatasource(id);
if (ds != null) {
try {
JcrMetadataAccess.ensureCheckoutNode(((JcrDatasource) ds).getNode().getParent());
((JcrDatasource) ds).getNode().remove();
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Unable to remove Datasource: " + id);
}
}
}
@Override
public DerivedDatasource ensureGenericDatasource(String name, String descr) {
DerivedDatasource genericDatasource = ensureDatasource(name, descr, DerivedDatasource.class);
return genericDatasource;
}
@Override
public <D extends DatasourceDetails> Optional<D> ensureDatasourceDetails(@Nonnull final Datasource.ID id, @Nonnull final Class<D> type) {
try {
// Ensure the data source exists
final Optional<JcrUserDatasource> parent = Optional.ofNullable(getDatasource(id))
.filter(JcrUserDatasource.class::isInstance)
.map(JcrUserDatasource.class::cast);
if (!parent.isPresent()) {
return Optional.empty();
}
// Create the details
final Class<? extends JcrDatasourceDetails> implType = JcrUserDatasource.resolveDetailsClass(type);
final boolean isNew = !hasEntityNode(parent.get().getPath(), JcrUserDatasource.DETAILS);
final Node node = findOrCreateEntityNode(parent.get().getPath(), JcrUserDatasource.DETAILS, implType);
@SuppressWarnings("unchecked") final D details = (D) JcrUtil.createJcrObject(node, implType);
// Re-assign permissions to data source
if (isNew) {
final UsernamePrincipal owner = parent
.map(JcrUserDatasource::getOwner)
.map(Principal::getName)
.map(UsernamePrincipal::new)
.orElse(JcrMetadataAccess.getActiveUser());
if (accessController.isEntityAccessControlled()) {
final List<SecurityRole> roles = roleProvider.getEntityRoles(SecurityRole.DATASOURCE);
actionsProvider.getAvailableActions(AllowedActions.DATASOURCE)
.ifPresent(actions -> parent.get().enableAccessControl((JcrAllowedActions) actions, owner, roles));
} else {
actionsProvider.getAvailableActions(AllowedActions.DATASOURCE)
.ifPresent(actions -> parent.get().disableAccessControl((JcrAllowedActions) actions, owner));
}
}
return Optional.of(details);
} catch (final IllegalArgumentException e) {
throw new MetadataException("Unable to create datasource details: " + type, e);
}
}
public Datasource.ID resolveId(Serializable fid) {
return new JcrDatasource.DatasourceId(fid);
}
private <J extends JcrDatasource> J createImpl(String name, String descr, Class<? extends Datasource> type) {
try {
JcrTool tool = new JcrTool();
Class<J> implType = deriveImplType(type);
Field folderField = FieldUtils.getField(implType, "PATH_NAME", true);
String subfolderName = (String) folderField.get(null);
String dsPath = EntityUtil.pathForDataSource();
Node dsNode = getSession().getNode(dsPath);
Node subfolderNode = tool.findOrCreateChild(dsNode, subfolderName, "nt:folder");
Map<String, Object> props = new HashMap<>();
props.put(JcrDatasource.SYSTEM_NAME, name);
String encodedName = org.modeshape.jcr.value.Path.DEFAULT_ENCODER.encode(name);
final boolean isNew = !hasEntityNode(subfolderNode.getPath(), encodedName);
@SuppressWarnings("unchecked")
J datasource = (J) findOrCreateEntity(subfolderNode.getPath(), encodedName, implType, props);
if (isNew && JcrUserDatasource.class.isAssignableFrom(type)) {
if (this.accessController.isEntityAccessControlled()) {
final List<SecurityRole> roles = roleProvider.getEntityRoles(SecurityRole.DATASOURCE);
actionsProvider.getAvailableActions(AllowedActions.DATASOURCE)
.ifPresent(actions -> ((JcrUserDatasource) datasource).enableAccessControl((JcrAllowedActions) actions, JcrMetadataAccess.getActiveUser(), roles));
} else {
actionsProvider.getAvailableActions(AllowedActions.DATASOURCE)
.ifPresent(actions -> ((JcrUserDatasource) datasource).disableAccessControl((JcrAllowedActions) actions, JcrMetadataAccess.getActiveUser()));
}
}
datasource.setTitle(name);
datasource.setDescription(descr);
return datasource;
} catch (IllegalArgumentException | IllegalAccessException | RepositoryException e) {
throw new MetadataException("Unable to create datasource: " + type, e);
}
}
@SuppressWarnings("unchecked")
private <J extends JcrDatasource> Class<J> deriveImplType(Class<? extends Datasource> domainType) {
Class<? extends JcrDatasource> implType = DOMAIN_TYPES_MAP.get(domainType);
if (implType != null) {
return (Class<J>) implType;
} else {
throw new MetadataException("No datasource implementation found for type: " + domainType);
}
}
// TODO Replace this implementation with a query restricting version. This is just a
// workaround that filters on the results set.
private static class Criteria extends AbstractMetadataCriteria<DatasourceCriteria>
implements DatasourceCriteria, Predicate<Datasource>, Comparator<Datasource> {
private String name;
private DateTime createdOn;
private DateTime createdAfter;
private DateTime createdBefore;
private Class<? extends Datasource> type;
@Override
public boolean test(Datasource input) {
if (this.type != null && !this.type.isAssignableFrom(input.getClass())) {
return false;
}
if (this.name != null && !name.equals(input.getName())) {
return false;
}
if (this.createdOn != null && !this.createdOn.equals(input.getCreatedTime())) {
return false;
}
if (this.createdAfter != null && !this.createdAfter.isBefore(input.getCreatedTime())) {
return false;
}
if (this.createdBefore != null && !this.createdBefore.isBefore(input.getCreatedTime())) {
return false;
}
return true;
}
@Override
public int compare(Datasource o1, Datasource o2) {
return o2.getCreatedTime().compareTo(o1.getCreatedTime());
}
@Override
public DatasourceCriteria name(String name) {
this.name = name;
return this;
}
@Override
public DatasourceCriteria createdOn(DateTime time) {
this.createdOn = time;
return this;
}
@Override
public DatasourceCriteria createdAfter(DateTime time) {
this.createdAfter = time;
return this;
}
@Override
public DatasourceCriteria createdBefore(DateTime time) {
this.createdBefore = time;
return this;
}
@Override
public DatasourceCriteria type(Class<? extends Datasource> type) {
this.type = type;
return this;
}
}
}