package com.thinkbiganalytics.feedmgr.service.datasource;
/*-
* #%L
* kylo-feed-manager-controller
* %%
* 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.feedmgr.service.security.SecurityService;
import com.thinkbiganalytics.metadata.api.datasource.DatasourceDetails;
import com.thinkbiganalytics.metadata.api.datasource.DatasourceProvider;
import com.thinkbiganalytics.metadata.api.datasource.JdbcDatasourceDetails;
import com.thinkbiganalytics.metadata.api.datasource.security.DatasourceAccessControl;
import com.thinkbiganalytics.metadata.rest.model.data.Datasource;
import com.thinkbiganalytics.metadata.rest.model.data.DerivedDatasource;
import com.thinkbiganalytics.metadata.rest.model.data.JdbcDatasource;
import com.thinkbiganalytics.metadata.rest.model.data.UserDatasource;
import com.thinkbiganalytics.metadata.rest.model.feed.Feed;
import com.thinkbiganalytics.nifi.rest.client.NiFiControllerServicesRestClient;
import com.thinkbiganalytics.nifi.rest.client.NiFiRestClient;
import com.thinkbiganalytics.nifi.rest.client.NifiClientRuntimeException;
import com.thinkbiganalytics.nifi.rest.client.NifiComponentNotFoundException;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.web.api.dto.ControllerServiceDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.crypto.encrypt.TextEncryptor;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nonnull;
/**
* Transform {@code Datasource}s between domain and REST objects.
*/
public class DatasourceModelTransform {
private static final Logger log = LoggerFactory.getLogger(DatasourceModelTransform.class);
/**
* Level of detail to include when transforming objects.
*/
public enum Level {
/**
* Include sensitive fields in result
*/
ADMIN,
/**
* Include everything except sensitive fields in result
*/
FULL,
/**
* Include basic field and connections in result
*/
CONNECTIONS,
/**
* Include only basic fields in result
*/
BASIC
}
/**
* Provides access to {@code Datasource} domain objects
*/
@Nonnull
private final DatasourceProvider datasourceProvider;
/**
* Encrypts strings
*/
@Nonnull
private final TextEncryptor encryptor;
/**
* NiFi REST client
*/
@Nonnull
private final NiFiRestClient nifiRestClient;
/**
* Security service
*/
@Nonnull
private final SecurityService securityService;
/**
* Constructs a {@code DatasourceModelTransform}.
*
* @param datasourceProvider the {@code Datasource} domain object provider
* @param encryptor the text encryptor
* @param nifiRestClient the NiFi REST client
* @param securityService the security service
*/
public DatasourceModelTransform(@Nonnull final DatasourceProvider datasourceProvider, @Nonnull final TextEncryptor encryptor, @Nonnull final NiFiRestClient nifiRestClient,
@Nonnull final SecurityService securityService) {
this.datasourceProvider = datasourceProvider;
this.encryptor = encryptor;
this.nifiRestClient = nifiRestClient;
this.securityService = securityService;
}
/**
* Transforms the specified domain object to a REST object.
*
* @param domain the domain object
* @param level the level of detail
* @return the REST object
* @throws IllegalArgumentException if the domain object cannot be converted
*/
public Datasource toDatasource(@Nonnull final com.thinkbiganalytics.metadata.api.datasource.Datasource domain, @Nonnull final Level level) {
if (domain instanceof com.thinkbiganalytics.metadata.api.datasource.DerivedDatasource) {
final DerivedDatasource ds = new DerivedDatasource();
updateDatasource(ds, (com.thinkbiganalytics.metadata.api.datasource.DerivedDatasource) domain, level);
return ds;
} else if (domain instanceof com.thinkbiganalytics.metadata.api.datasource.UserDatasource) {
return toDatasource((com.thinkbiganalytics.metadata.api.datasource.UserDatasource) domain, level);
} else {
throw new IllegalArgumentException("Not a supported datasource class: " + domain.getClass());
}
}
/**
* Transforms the specified domain object to a REST object.
*
* @param domain the domain object
* @param level the level of detail
* @return the REST object
* @throws IllegalArgumentException if the domain object cannot be converted
*/
public UserDatasource toDatasource(@Nonnull final com.thinkbiganalytics.metadata.api.datasource.UserDatasource domain, @Nonnull final Level level) {
final DatasourceDetails details = domain.getDetails().orElse(null);
if (details == null) {
final UserDatasource userDatasource = new UserDatasource();
updateDatasource(userDatasource, domain, level);
return userDatasource;
} else if (details instanceof JdbcDatasourceDetails) {
final JdbcDatasource jdbcDatasource = new JdbcDatasource();
updateDatasource(jdbcDatasource, domain, level);
return jdbcDatasource;
} else {
throw new IllegalArgumentException("Not a supported datasource details class: " + details.getClass());
}
}
/**
* Transforms the specified REST object to a domain object.
*
* @param ds the REST object
* @return the domain object
* @throws IllegalArgumentException if the REST object cannot be converted
*/
public com.thinkbiganalytics.metadata.api.datasource.Datasource toDomain(@Nonnull final Datasource ds) {
if (ds instanceof UserDatasource) {
final com.thinkbiganalytics.metadata.api.datasource.UserDatasource domain;
final boolean isNew = (ds.getId() == null);
if (isNew) {
domain = datasourceProvider.ensureDatasource(ds.getName(), ds.getDescription(), com.thinkbiganalytics.metadata.api.datasource.UserDatasource.class);
ds.setId(domain.getId().toString());
} else {
final com.thinkbiganalytics.metadata.api.datasource.Datasource.ID id = datasourceProvider.resolve(ds.getId());
domain = (com.thinkbiganalytics.metadata.api.datasource.UserDatasource) datasourceProvider.getDatasource(id);
}
if (ds instanceof JdbcDatasource) {
if (isNew) {
datasourceProvider.ensureDatasourceDetails(domain.getId(), JdbcDatasourceDetails.class);
}
updateDomain(domain, (JdbcDatasource) ds);
} else {
updateDomain(domain, (UserDatasource) ds);
}
return domain;
} else {
throw new IllegalArgumentException("Not a supported user datasource class: " + ds.getClass());
}
}
/**
* Updates the specified REST object with properties from the specified domain object.
*
* @param ds the REST object
* @param domain the domain object
* @param level the level of detail
*/
private void updateDatasource(@Nonnull final Datasource ds, @Nonnull final com.thinkbiganalytics.metadata.api.datasource.Datasource domain, @Nonnull final Level level) {
ds.setId(domain.getId().toString());
ds.setDescription(domain.getDescription());
ds.setName(domain.getName());
// Add connections if level matches
if (level.compareTo(Level.CONNECTIONS) <= 0) {
for (com.thinkbiganalytics.metadata.api.feed.FeedSource domainSrc : domain.getFeedSources()) {
Feed feed = new Feed();
feed.setDisplayName(domainSrc.getFeed().getDisplayName());
feed.setId(domainSrc.getFeed().getId().toString());
feed.setState(Feed.State.valueOf(domainSrc.getFeed().getState().name()));
feed.setSystemName(domainSrc.getFeed().getName());
feed.setModifiedTime(domainSrc.getFeed().getModifiedTime());
ds.getSourceForFeeds().add(feed);
}
for (com.thinkbiganalytics.metadata.api.feed.FeedDestination domainDest : domain.getFeedDestinations()) {
Feed feed = new Feed();
feed.setDisplayName(domainDest.getFeed().getDisplayName());
feed.setId(domainDest.getFeed().getId().toString());
feed.setState(Feed.State.valueOf(domainDest.getFeed().getState().name()));
feed.setSystemName(domainDest.getFeed().getName());
feed.setModifiedTime(domainDest.getFeed().getModifiedTime());
ds.getDestinationForFeeds().add(feed);
}
}
}
/**
* Updates the specified REST object with properties from the specified domain object.
*
* @param ds the REST object
* @param domain the domain object
* @param level the level of detail
*/
public void updateDatasource(@Nonnull final DerivedDatasource ds, @Nonnull final com.thinkbiganalytics.metadata.api.datasource.DerivedDatasource domain, @Nonnull final Level level) {
updateDatasource((Datasource) ds, domain, level);
ds.setProperties(domain.getProperties());
ds.setDatasourceType(domain.getDatasourceType());
}
/**
* Updates the specified REST object with properties from the specified domain object.
*
* @param ds the REST object
* @param domain the domain object
* @param level the level of detail
*/
private void updateDatasource(@Nonnull final JdbcDatasource ds, @Nonnull final com.thinkbiganalytics.metadata.api.datasource.UserDatasource domain, @Nonnull final Level level) {
updateDatasource((UserDatasource) ds, domain, level);
domain.getDetails()
.map(JdbcDatasourceDetails.class::cast)
.ifPresent(details -> {
details.getControllerServiceId().ifPresent(ds::setControllerServiceId);
if (level.compareTo(Level.ADMIN) <= 0) {
ds.setPassword(encryptor.decrypt(details.getPassword()));
}
if (level.compareTo(Level.FULL) <= 0) {
// Fetch database properties from NiFi
details.getControllerServiceId()
.flatMap(id -> nifiRestClient.controllerServices().findById(id))
.ifPresent(controllerService -> {
ds.setDatabaseConnectionUrl(controllerService.getProperties().get(DatasourceConstants.DATABASE_CONNECTION_URL));
ds.setDatabaseDriverClassName(controllerService.getProperties().get(DatasourceConstants.DATABASE_DRIVER_CLASS_NAME));
ds.setDatabaseDriverLocation(controllerService.getProperties().get(DatasourceConstants.DATABASE_DRIVER_LOCATION));
ds.setDatabaseUser(controllerService.getProperties().get(DatasourceConstants.DATABASE_USER));
});
}
});
}
/**
* Updates the specified REST object with properties from the specified domain object.
*
* @param ds the REST object
* @param domain the domain object
* @param level the level of detail
*/
private void updateDatasource(@Nonnull final UserDatasource ds, @Nonnull final com.thinkbiganalytics.metadata.api.datasource.UserDatasource domain, @Nonnull final Level level) {
updateDatasource((Datasource) ds, domain, level);
ds.setType(domain.getType());
}
/**
* Updates the specified domain object with properties from the specified REST object.
*
* @param domain the domain object
* @param ds the REST object
*/
private void updateDomain(@Nonnull final com.thinkbiganalytics.metadata.api.datasource.UserDatasource domain, @Nonnull final JdbcDatasource ds) {
updateDomain(domain, (UserDatasource) ds);
domain.getDetails()
.map(JdbcDatasourceDetails.class::cast)
.ifPresent(details -> {
// Look for changed properties
final Map<String, String> properties = new HashMap<>();
if (StringUtils.isNotBlank(ds.getDatabaseConnectionUrl())) {
properties.put(DatasourceConstants.DATABASE_CONNECTION_URL, ds.getDatabaseConnectionUrl());
}
if (StringUtils.isNotBlank(ds.getDatabaseDriverClassName())) {
properties.put(DatasourceConstants.DATABASE_DRIVER_CLASS_NAME, ds.getDatabaseDriverClassName());
}
if (ds.getDatabaseDriverLocation() != null) {
properties.put(DatasourceConstants.DATABASE_DRIVER_LOCATION, ds.getDatabaseDriverLocation());
}
if (ds.getDatabaseUser() != null) {
properties.put(DatasourceConstants.DATABASE_USER, ds.getDatabaseUser());
}
if (ds.getPassword() != null) {
details.setPassword(encryptor.encrypt(ds.getPassword()));
properties.put(DatasourceConstants.PASSWORD, StringUtils.isNotEmpty(ds.getPassword()) ? ds.getPassword() : null);
}
// Update or create the controller service
ControllerServiceDTO controllerService = null;
if (details.getControllerServiceId().isPresent()) {
controllerService = new ControllerServiceDTO();
controllerService.setId(details.getControllerServiceId().get());
controllerService.setName(ds.getName());
controllerService.setComments(ds.getDescription());
controllerService.setProperties(properties);
try {
nifiRestClient.controllerServices().updateStateById(controllerService.getId(), NiFiControllerServicesRestClient.State.DISABLED);
nifiRestClient.controllerServices().update(controllerService);
nifiRestClient.controllerServices().updateStateById(controllerService.getId(), NiFiControllerServicesRestClient.State.ENABLED);
ds.setControllerServiceId(controllerService.getId());
} catch (final NifiComponentNotFoundException e) {
log.warn("Controller service is missing for datasource: {}", domain.getId(), e);
controllerService = null;
}
}
if (controllerService == null) {
controllerService = new ControllerServiceDTO();
controllerService.setType("org.apache.nifi.dbcp.DBCPConnectionPool");
controllerService.setName(ds.getName());
controllerService.setComments(ds.getDescription());
controllerService.setProperties(properties);
final ControllerServiceDTO newControllerService = nifiRestClient.controllerServices().create(controllerService);
try {
nifiRestClient.controllerServices().updateStateById(newControllerService.getId(), NiFiControllerServicesRestClient.State.ENABLED);
} catch (final NifiClientRuntimeException nifiException) {
log.error("Failed to enable controller service for datasource: {}", domain.getId(), nifiException);
nifiRestClient.controllerServices().disableAndDeleteAsync(newControllerService.getId());
throw nifiException;
}
details.setControllerServiceId(newControllerService.getId());
ds.setControllerServiceId(newControllerService.getId());
}
});
}
/**
* Updates the specified domain object with properties from the specified REST object.
*
* @param domain the domain object
* @param ds the REST object
*/
private void updateDomain(@Nonnull final com.thinkbiganalytics.metadata.api.datasource.UserDatasource domain, @Nonnull final UserDatasource ds) {
// Update properties
domain.setDescription(ds.getDescription());
domain.setName(ds.getName());
domain.setType(ds.getType());
// Update access control
if (domain.getAllowedActions().hasPermission(DatasourceAccessControl.CHANGE_PERMS)) {
ds.toRoleMembershipChangeList().forEach(roleMembershipChange -> securityService.changeDatasourceRoleMemberships(ds.getId(), roleMembershipChange));
}
}
}