/**
* Copyright (c) Codice Foundation
* <p/>
* This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or any later version.
* <p/>
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details. A copy of the GNU Lesser General Public License
* is distributed along with this program and can be found at
* <http://www.gnu.org/licenses/lgpl.html>.
*/
package org.codice.ddf.confluence.source;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.ws.rs.core.Response;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
import org.boon.json.JsonFactory;
import org.boon.json.ObjectMapper;
import org.codice.ddf.configuration.PropertyResolver;
import org.codice.ddf.confluence.api.SearchResource;
import org.codice.ddf.cxf.SecureCxfClientFactory;
import org.opengis.filter.sort.SortBy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ddf.catalog.data.ContentType;
import ddf.catalog.data.Metacard;
import ddf.catalog.data.Result;
import ddf.catalog.data.impl.AttributeImpl;
import ddf.catalog.data.impl.ResultImpl;
import ddf.catalog.filter.FilterAdapter;
import ddf.catalog.operation.Query;
import ddf.catalog.operation.QueryRequest;
import ddf.catalog.operation.ResourceResponse;
import ddf.catalog.operation.SourceResponse;
import ddf.catalog.operation.impl.SourceResponseImpl;
import ddf.catalog.resource.ResourceNotFoundException;
import ddf.catalog.resource.ResourceNotSupportedException;
import ddf.catalog.resource.ResourceReader;
import ddf.catalog.service.ConfiguredService;
import ddf.catalog.source.FederatedSource;
import ddf.catalog.source.SourceMonitor;
import ddf.catalog.source.UnsupportedQueryException;
import ddf.catalog.transform.CatalogTransformerException;
import ddf.catalog.util.impl.MaskableImpl;
import ddf.security.encryption.EncryptionService;
import ddf.security.permission.Permissions;
public class ConfluenceSource extends MaskableImpl implements FederatedSource, ConfiguredService {
private static final Logger LOGGER = LoggerFactory.getLogger(ConfluenceSource.class);
private static final ObjectMapper MAPPER = JsonFactory.create();
private static final String USERNAME = "username";
private static final String PASSWORD = "password";
private static final String BASE_URL = "baseUrl";
private String endpointUrl;
private String configurationPid;
private String username;
private String password;
private String expandedSections = "";
private Boolean includeArchivedSpaces = false;
private Boolean includePageContent = false;
private Boolean excludeSpaces = false;
private List<String> confluenceSpaces = new ArrayList<>();
private final EncryptionService encryptionService;
private final FilterAdapter filterAdapter;
private final ResourceReader resourceReader;
private final ConfluenceInputTransformer transformer;
private SecureCxfClientFactory<SearchResource> factory;
private boolean lastAvailable;
private Date lastAvailableDate = null;
private long availabilityPollInterval = TimeUnit.SECONDS.toMillis(60);
private Set<SourceMonitor> sourceMonitors = new HashSet<>();
private Map<String, Set<String>> additionalMetacardAttributes = new HashMap<>();
public ConfluenceSource(FilterAdapter adapter, EncryptionService encryptionService,
ConfluenceInputTransformer transformer, ResourceReader reader) {
this.filterAdapter = adapter;
this.encryptionService = encryptionService;
this.transformer = transformer;
this.resourceReader = reader;
}
public void init() {
if (StringUtils.isNotBlank(username) && StringUtils.isNotBlank(password)) {
factory = new SecureCxfClientFactory<SearchResource>(endpointUrl,
SearchResource.class,
username,
password);
} else {
factory = new SecureCxfClientFactory<SearchResource>(endpointUrl, SearchResource.class);
}
}
@Override
public String getConfigurationPid() {
return configurationPid;
}
@Override
public void setConfigurationPid(String configurationPid) {
this.configurationPid = configurationPid;
}
@Override
public boolean isAvailable() {
boolean isAvailable = false;
if (!lastAvailable || (lastAvailableDate.before(new Date(
System.currentTimeMillis() - availabilityPollInterval)))) {
Response response = null;
try {
response = getClientFactory().getWebClient()
.head();
} catch (Exception e) {
LOGGER.debug("Web Client was unable to connect to endpoint.", e);
}
if (response != null && !(response.getStatus() >= HttpStatus.SC_NOT_FOUND
|| response.getStatus() == HttpStatus.SC_BAD_REQUEST
|| response.getStatus() == HttpStatus.SC_PAYMENT_REQUIRED)) {
isAvailable = true;
lastAvailableDate = new Date();
}
} else {
isAvailable = lastAvailable;
}
if (lastAvailable != isAvailable) {
for (SourceMonitor monitor : this.sourceMonitors) {
if (isAvailable) {
monitor.setAvailable();
} else {
monitor.setUnavailable();
}
}
}
lastAvailable = isAvailable;
return isAvailable;
}
@Override
public boolean isAvailable(SourceMonitor callback) {
sourceMonitors.add(callback);
return isAvailable();
}
@Override
public SourceResponse query(QueryRequest request) throws UnsupportedQueryException {
Query query = request.getQuery();
ConfluenceFilterDelegate confluenceDelegate = new ConfluenceFilterDelegate();
String cql = filterAdapter.adapt(query, confluenceDelegate);
if (!confluenceDelegate.isConfluenceQuery() || (StringUtils.isEmpty(cql) && (
confluenceSpaces.isEmpty() || !confluenceDelegate.isWildCardQuery()))) {
return new SourceResponseImpl(request, Collections.emptyList());
}
cql = getSortedQuery(query.getSortBy(), getSpaceQuery(cql));
LOGGER.debug(cql);
String finalExpandedSections = expandedSections;
if (includePageContent) {
finalExpandedSections += ",body.view";
}
SearchResource confluence = getClientFactory().getClient();
String cqlContext = null;
String excerpt = null;
Response confluenceResponse = confluence.search(cql,
cqlContext,
excerpt,
finalExpandedSections,
query.getStartIndex() - 1,
query.getPageSize(),
includeArchivedSpaces);
InputStream stream = null;
Object entityObj = confluenceResponse.getEntity();
if (entityObj != null) {
stream = (InputStream) entityObj;
}
if (Response.Status.OK.getStatusCode() != confluenceResponse.getStatus()) {
String error = "";
try {
if (stream != null) {
error = IOUtils.toString(stream);
}
} catch (IOException ioe) {
LOGGER.debug("Could not convert error message to a string for output.", ioe);
}
throw new UnsupportedQueryException(String.format(
"Received error code from remote source (status %s ): %s",
confluenceResponse.getStatus(),
error));
}
try {
List<Result> results = transformer.transformConfluenceResponse(stream)
.stream()
.map(this::getUpdatedResult)
.collect(Collectors.toList());
return new SourceResponseImpl(request, results);
} catch (IOException | CatalogTransformerException e) {
throw new UnsupportedQueryException("Exception processing results from Confluence");
}
}
@Override
public Set<ContentType> getContentTypes() {
return Collections.emptySet();
}
@Override
public ResourceResponse retrieveResource(URI uri, Map<String, Serializable> arguments)
throws IOException, ResourceNotFoundException, ResourceNotSupportedException {
if (StringUtils.isNotBlank(username) && StringUtils.isNotBlank(password)) {
arguments.put(USERNAME, username);
arguments.put(PASSWORD, password);
}
return resourceReader.retrieveResource(uri, arguments);
}
@Override
public Set<String> getSupportedSchemes() {
return Collections.emptySet();
}
@Override
public Set<String> getOptions(Metacard metacard) {
return Collections.emptySet();
}
public void setAvailabilityPollInterval(long availabilityPollInterval) {
this.availabilityPollInterval = availabilityPollInterval;
}
public void setEndpointUrl(String endpointUrl) {
if (endpointUrl != null) {
endpointUrl = endpointUrl.trim();
}
this.endpointUrl = PropertyResolver.resolveProperties(endpointUrl);
init();
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
if (encryptionService != null) {
this.password = encryptionService.decryptValue(password);
}
}
public void setExpandedSections(List<String> expandedSections) {
if (expandedSections == null) {
this.expandedSections = "";
return;
}
this.expandedSections = expandedSections.stream()
.collect(Collectors.joining(","));
}
public void setIncludeArchivedSpaces(Boolean includeArchivedSpaces) {
this.includeArchivedSpaces = includeArchivedSpaces;
}
public void setIncludePageContent(Boolean includePageContent) {
this.includePageContent = includePageContent;
}
public void setConfluenceSpaces(List<String> confluenceSpace) {
this.confluenceSpaces = confluenceSpace;
}
public void setExcludeSpaces(Boolean excludeSpaces) {
this.excludeSpaces = excludeSpaces;
}
public void setAdditionalAttributes(List<String> attributes) {
additionalMetacardAttributes = Permissions.parsePermissionsFromString(attributes);
}
public SecureCxfClientFactory<SearchResource> getClientFactory() {
return factory;
}
private Result getUpdatedResult(Metacard metacard) {
metacard.setSourceId(this.getId());
for (Map.Entry<String, Set<String>> entry : additionalMetacardAttributes.entrySet()) {
Set<String> value = entry.getValue();
if (value.size() == 1) {
metacard.setAttribute(new AttributeImpl(entry.getKey(),
value.iterator()
.next()));
} else {
metacard.setAttribute(new AttributeImpl(entry.getKey(),
new ArrayList<String>(value)));
}
}
return new ResultImpl(metacard);
}
private String getSortedQuery(SortBy sort, String query) {
if (sort != null && sort.getPropertyName() != null && sort.getPropertyName()
.getPropertyName() != null) {
String sortProperty = sort.getPropertyName()
.getPropertyName();
if (ConfluenceFilterDelegate.QUERY_PARAMETERS.containsKey(sortProperty)) {
query = String.format("%s order by %s %s",
query,
ConfluenceFilterDelegate.QUERY_PARAMETERS.get(sortProperty)
.getParamterName(),
sort.getSortOrder()
.toSQL());
}
}
return query;
}
private String getSpaceQuery(String query) {
if (!confluenceSpaces.isEmpty()) {
StringBuilder sb = new StringBuilder();
sb.append(query);
if (StringUtils.isNotEmpty(query.trim())) {
sb.append(" AND ");
}
sb.append("space");
if (excludeSpaces) {
sb.append(" NOT IN (");
} else {
sb.append(" IN (");
}
sb.append(String.join(", ", confluenceSpaces));
sb.append(")");
return sb.toString();
}
return query;
}
}