/**
* 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.spatial.ogc.csw.catalog.common.source;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.xml.bind.JAXBElement;
import org.codice.ddf.cxf.SecureCxfClientFactory;
import org.codice.ddf.spatial.ogc.csw.catalog.common.Csw;
import org.codice.ddf.spatial.ogc.csw.catalog.common.CswConstants;
import org.codice.ddf.spatial.ogc.csw.catalog.common.CswException;
import org.codice.ddf.spatial.ogc.csw.catalog.common.CswSourceConfiguration;
import org.codice.ddf.spatial.ogc.csw.catalog.common.converter.DefaultCswRecordMap;
import org.codice.ddf.spatial.ogc.csw.catalog.common.transaction.CswTransactionRequest;
import org.codice.ddf.spatial.ogc.csw.catalog.common.transaction.DeleteAction;
import org.codice.ddf.spatial.ogc.csw.catalog.common.transaction.InsertAction;
import org.codice.ddf.spatial.ogc.csw.catalog.common.transaction.UpdateAction;
import org.codice.ddf.spatial.ogc.csw.catalog.common.transformer.TransformerManager;
import org.opengis.filter.Filter;
import org.osgi.framework.BundleContext;
import com.thoughtworks.xstream.converters.Converter;
import ddf.catalog.Constants;
import ddf.catalog.data.Metacard;
import ddf.catalog.data.Result;
import ddf.catalog.data.types.Core;
import ddf.catalog.filter.FilterDelegate;
import ddf.catalog.operation.CreateRequest;
import ddf.catalog.operation.CreateResponse;
import ddf.catalog.operation.DeleteRequest;
import ddf.catalog.operation.DeleteResponse;
import ddf.catalog.operation.OperationTransaction;
import ddf.catalog.operation.ProcessingDetails;
import ddf.catalog.operation.Query;
import ddf.catalog.operation.QueryRequest;
import ddf.catalog.operation.SourceResponse;
import ddf.catalog.operation.UpdateRequest;
import ddf.catalog.operation.UpdateResponse;
import ddf.catalog.operation.impl.CreateResponseImpl;
import ddf.catalog.operation.impl.DeleteResponseImpl;
import ddf.catalog.operation.impl.ProcessingDetailsImpl;
import ddf.catalog.operation.impl.QueryImpl;
import ddf.catalog.operation.impl.QueryRequestImpl;
import ddf.catalog.operation.impl.UpdateResponseImpl;
import ddf.catalog.source.CatalogStore;
import ddf.catalog.source.IngestException;
import ddf.catalog.source.UnsupportedQueryException;
import ddf.security.SecurityConstants;
import ddf.security.Subject;
import ddf.security.encryption.EncryptionService;
import net.opengis.cat.csw.v_2_0_2.BriefRecordType;
import net.opengis.cat.csw.v_2_0_2.DeleteType;
import net.opengis.cat.csw.v_2_0_2.InsertResultType;
import net.opengis.cat.csw.v_2_0_2.QueryConstraintType;
import net.opengis.cat.csw.v_2_0_2.TransactionResponseType;
import net.opengis.cat.csw.v_2_0_2.dc.elements.SimpleLiteral;
import net.opengis.filter.v_1_1_0.FilterType;
public abstract class AbstractCswStore extends AbstractCswSource implements CatalogStore {
protected TransformerManager schemaTransformerManager;
protected MessageBodyWriter<CswTransactionRequest> cswTransactionWriter;
/**
* Instantiates a CswStore. This constructor is for unit tests
*
* @param context The {@link BundleContext} from the OSGi Framework
* @param cswSourceConfiguration the configuration of this source
* @param provider transform provider to transform results
* @param factory client factory already configured for this source
*/
public AbstractCswStore(BundleContext context, CswSourceConfiguration cswSourceConfiguration,
Converter provider, SecureCxfClientFactory factory,
EncryptionService encryptionService) {
super(context, cswSourceConfiguration, provider, factory, encryptionService);
}
/**
* Instantiates a CswStore.
*/
public AbstractCswStore(EncryptionService encryptionService) {
super(encryptionService);
}
@Override
public CreateResponse create(CreateRequest createRequest) throws IngestException {
Map<String, Serializable> properties = new HashMap<>();
validateOperation();
Subject subject =
(Subject) createRequest.getPropertyValue(SecurityConstants.SECURITY_SUBJECT);
Csw csw = factory.getClientForSubject(subject);
CswTransactionRequest transactionRequest = getTransactionRequest();
List<Metacard> metacards = createRequest.getMetacards();
List<String> metacardIds = metacards.stream()
.map(Metacard::getId)
.collect(Collectors.toList());
List<Metacard> createdMetacards = new ArrayList<>();
List<Filter> createdMetacardFilters;
HashSet<ProcessingDetails> errors = new HashSet<>();
String insertTypeName = schemaTransformerManager.getTransformerIdForSchema(
cswSourceConfiguration.getOutputSchema());
if (insertTypeName == null) {
throw new IngestException("Could not find transformer for output schema "
+ cswSourceConfiguration.getOutputSchema());
}
transactionRequest.getInsertActions()
.add(new InsertAction(insertTypeName, null, metacards));
try {
TransactionResponseType response = csw.transaction(transactionRequest);
Set<String> processedIds = new HashSet<>();
//dive down into the response to get the created ID's. We need these so we can query
//the source again to get the created metacards and put them in the result
createdMetacardFilters = response.getInsertResult()
.stream()
.map(InsertResultType::getBriefRecord)
.flatMap(Collection::stream)
.map(BriefRecordType::getIdentifier)
.flatMap(Collection::stream)
.map(JAXBElement::getValue)
.map(SimpleLiteral::getContent)
.flatMap(Collection::stream)
.map(id -> {
processedIds.add(id);
return filterBuilder.attribute(Core.ID)
.is()
.equalTo()
.text(id);
})
.collect(Collectors.toList());
metacardIds.removeAll(processedIds);
errors.addAll(metacardIds.stream()
.map(id -> new ProcessingDetailsImpl(id, null, "Failed to create metacard"))
.collect(Collectors.toList()));
} catch (CswException e) {
throw new IngestException("Csw Transaction Failed : ", e);
}
try {
createdMetacards = transactionQuery(createdMetacardFilters, subject);
} catch (UnsupportedQueryException e) {
errors.add(new ProcessingDetailsImpl(this.getId(),
e,
"Failed to retrieve newly created metacards"));
}
return new CreateResponseImpl(createRequest, properties, createdMetacards, errors);
}
@Override
public UpdateResponse update(UpdateRequest updateRequest) throws IngestException {
Map<String, Serializable> properties = new HashMap<>();
validateOperation();
Subject subject =
(Subject) updateRequest.getPropertyValue(SecurityConstants.SECURITY_SUBJECT);
Csw csw = factory.getClientForSubject(subject);
CswTransactionRequest transactionRequest = getTransactionRequest();
OperationTransaction opTrans = (OperationTransaction) updateRequest.getPropertyValue(
Constants.OPERATION_TRANSACTION_KEY);
String insertTypeName = schemaTransformerManager.getTransformerIdForSchema(
cswSourceConfiguration.getOutputSchema());
HashSet<ProcessingDetails> errors = new HashSet<>();
if (insertTypeName == null) {
insertTypeName = CswConstants.CSW_RECORD;
}
ArrayList<Metacard> updatedMetacards = new ArrayList<>(updateRequest.getUpdates()
.size());
ArrayList<Filter> updatedMetacardFilters = new ArrayList<>(updateRequest.getUpdates()
.size());
for (Map.Entry<Serializable, Metacard> update : updateRequest.getUpdates()) {
Metacard metacard = update.getValue();
properties.put(metacard.getId(), metacard);
updatedMetacardFilters.add(filterBuilder.attribute(updateRequest.getAttributeName())
.is()
.equalTo()
.text(update.getKey()
.toString()));
transactionRequest.getUpdateActions()
.add(new UpdateAction(metacard, insertTypeName, null));
}
try {
TransactionResponseType response = csw.transaction(transactionRequest);
if (response.getTransactionSummary()
.getTotalUpdated()
.longValue() != updateRequest.getUpdates()
.size()) {
errors.add(new ProcessingDetailsImpl(this.getId(),
null,
"One or more updates failed"));
}
} catch (CswException e) {
throw new IngestException("Csw Transaction Failed.", e);
}
try {
updatedMetacards.addAll(transactionQuery(updatedMetacardFilters, subject));
} catch (UnsupportedQueryException e) {
errors.add(new ProcessingDetailsImpl(this.getId(),
e,
"Failed to retrieve updated metacards"));
}
return new UpdateResponseImpl(updateRequest,
properties,
updatedMetacards,
new ArrayList(opTrans.getPreviousStateMetacards()),
errors);
}
@Override
public DeleteResponse delete(DeleteRequest deleteRequest) throws IngestException {
Map<String, Serializable> properties = new HashMap<>();
validateOperation();
Subject subject =
(Subject) deleteRequest.getPropertyValue(SecurityConstants.SECURITY_SUBJECT);
Csw csw = factory.getClientForSubject(subject);
CswTransactionRequest transactionRequest = getTransactionRequest();
OperationTransaction opTrans = (OperationTransaction) deleteRequest.getPropertyValue(
Constants.OPERATION_TRANSACTION_KEY);
String typeName =
schemaTransformerManager.getTransformerIdForSchema(cswSourceConfiguration.getOutputSchema());
if (typeName == null) {
typeName = CswConstants.CSW_RECORD;
}
for (Serializable itemToDelete : deleteRequest.getAttributeValues()) {
try {
DeleteType deleteType = new DeleteType();
deleteType.setTypeName(typeName);
QueryConstraintType queryConstraintType = new QueryConstraintType();
Filter filter;
FilterType filterType;
filter = filterBuilder.attribute(deleteRequest.getAttributeName())
.is()
.equalTo()
.text(itemToDelete.toString());
filterType = filterAdapter.adapt(filter, cswFilterDelegate);
queryConstraintType.setCqlText(CswCqlTextFilter.getInstance()
.getCqlText(filterType));
deleteType.setConstraint(queryConstraintType);
DeleteAction deleteAction = new DeleteAction(deleteType,
DefaultCswRecordMap.getPrefixToUriMapping());
transactionRequest.getDeleteActions()
.add(deleteAction);
} catch (UnsupportedQueryException e) {
throw new IngestException("Unsupported Query.", e);
}
}
try {
TransactionResponseType response = csw.transaction(transactionRequest);
if (response.getTransactionSummary()
.getTotalDeleted()
.intValue() != deleteRequest.getAttributeValues()
.size()) {
throw new IngestException(
"Csw Transaction Failed. Number of metacards deleted did not match number requested.");
}
} catch (CswException e) {
throw new IngestException("Csw Transaction Failed", e);
}
return new DeleteResponseImpl(deleteRequest,
properties,
new ArrayList(opTrans.getPreviousStateMetacards()));
}
@Override
protected List<? extends Object> initProviders(Converter cswTransformProvider,
CswSourceConfiguration cswSourceConfiguration) {
List providers = new ArrayList(super.initProviders(cswTransformProvider,
cswSourceConfiguration));
providers.add(cswTransactionWriter);
return providers;
}
public void setSchemaTransformerManager(TransformerManager schemaTransformerManager) {
this.schemaTransformerManager = schemaTransformerManager;
}
public MessageBodyWriter<CswTransactionRequest> getCswTransactionWriter() {
return cswTransactionWriter;
}
public void setCswTransactionWriter(
MessageBodyWriter<CswTransactionRequest> cswTransactionWriter) {
this.cswTransactionWriter = cswTransactionWriter;
}
private List<Metacard> transactionQuery(List<Filter> idFilters, Subject subject)
throws UnsupportedQueryException {
Filter createFilter = filterBuilder.allOf(filterBuilder.anyOf(filterBuilder.attribute(Core.METACARD_TAGS)
.is()
.like()
.text(FilterDelegate.WILDCARD_CHAR),
filterBuilder.attribute(Core.METACARD_TAGS)
.empty()), filterBuilder.anyOf(idFilters));
Query query = new QueryImpl(createFilter);
Map<String, Serializable> properties = new HashMap<>();
properties.put(SecurityConstants.SECURITY_SUBJECT, subject);
QueryRequest queryRequest = new QueryRequestImpl(query, properties);
SourceResponse sourceResponse = query(queryRequest);
List<Result> results = sourceResponse.getResults();
return results.stream()
.map(Result::getMetacard)
.collect(Collectors.toList());
}
private CswTransactionRequest getTransactionRequest() {
CswTransactionRequest transactionRequest = new CswTransactionRequest();
transactionRequest.setVersion(CswConstants.VERSION_2_0_2);
transactionRequest.setService(CswConstants.CSW);
transactionRequest.setVerbose(true);
return transactionRequest;
}
protected void validateOperation() {
if (capabilities == null) {
throw new UnsupportedOperationException(
"The CSW Store is not available. Operations can not be performed on it.");
}
Optional result = capabilities.getOperationsMetadata()
.getOperation()
.stream()
.filter(e -> e.getName()
.equals(CswConstants.TRANSACTION))
.findFirst();
if (!result.isPresent()) {
throw new UnsupportedOperationException(
"This CSW Endpoint referenced by this store doesn't support the Transaction Operation");
}
}
}