/*
* Copyright (c) 2015 Data Harmonisation Panel
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution. If not, see <http://www.gnu.org/licenses/>.
*
* Contributors:
* Data Harmonisation Panel <http://www.dhpanel.eu>
*/
package eu.esdihumboldt.hale.io.wfs;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.net.Proxy;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.client.fluent.Executor;
import org.apache.http.client.fluent.Request;
import org.apache.http.client.fluent.Response;
import org.apache.http.entity.ContentType;
import org.apache.http.util.EntityUtils;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import de.fhg.igd.slf4jplus.ALogger;
import de.fhg.igd.slf4jplus.ALoggerFactory;
import eu.esdihumboldt.hale.common.core.io.IOProviderConfigurationException;
import eu.esdihumboldt.hale.common.core.io.ProgressIndicator;
import eu.esdihumboldt.hale.common.core.io.Value;
import eu.esdihumboldt.hale.common.core.io.config.UserPasswordCredentials;
import eu.esdihumboldt.hale.common.core.io.impl.SubtaskProgressIndicator;
import eu.esdihumboldt.hale.common.core.io.report.IOReport;
import eu.esdihumboldt.hale.common.core.io.report.IOReporter;
import eu.esdihumboldt.hale.common.core.io.report.impl.IOMessageImpl;
import eu.esdihumboldt.hale.common.core.io.supplier.LocatableOutputSupplier;
import eu.esdihumboldt.hale.common.instance.io.util.GeoInstanceWriterDecorator;
import eu.esdihumboldt.hale.io.gml.writer.XmlWrapper;
import eu.esdihumboldt.hale.io.gml.writer.internal.StreamGmlWriter;
import eu.esdihumboldt.util.http.ProxyUtil;
import eu.esdihumboldt.util.http.client.ClientProxyUtil;
import eu.esdihumboldt.util.http.client.fluent.FluentProxyUtil;
/**
* Base class for WFS writers that directly write to the WFS-T.
*
* @param <T> the XML/GML writer type
* @author Simon Templer
*/
@SuppressWarnings("restriction")
public abstract class AbstractWFSWriter<T extends StreamGmlWriter> extends
GeoInstanceWriterDecorator<T>implements WFSWriter, WFSConstants, UserPasswordCredentials {
private static final ALogger log = ALoggerFactory.getLogger(AbstractWFSWriter.class);
private LocatableOutputSupplier<? extends OutputStream> targetWfs;
private OutputStream currentExecuteStream;
private final LocatableOutputSupplier<? extends OutputStream> decorateeTarget = new LocatableOutputSupplier<OutputStream>() {
@Override
public OutputStream getOutput() throws IOException {
return currentExecuteStream;
}
@Override
public URI getLocation() {
return targetWfs.getLocation();
}
};
/**
* @param internalProvider the internal provider producing GML
*/
public AbstractWFSWriter(T internalProvider) {
super(internalProvider);
}
@Override
public void setWFSVersion(WFSVersion version) {
setParameter(PARAM_WFS_VERSION, Value.of(version.versionString));
}
@Override
public WFSVersion getWFSVersion() {
String versionString = getParameter(PARAM_WFS_VERSION).as(String.class);
if (versionString == null) {
return null;
}
else
return WFSVersion.fromString(versionString, null);
}
@Override
public void setTarget(LocatableOutputSupplier<? extends OutputStream> target) {
targetWfs = target;
if (targetWfs == null) {
super.setTarget(null);
}
else {
super.setTarget(decorateeTarget);
}
}
@Override
public LocatableOutputSupplier<? extends OutputStream> getTarget() {
return targetWfs;
}
@Override
public IOReport execute(ProgressIndicator progress)
throws IOProviderConfigurationException, IOException {
progress.begin("WFS Transaction", ProgressIndicator.UNKNOWN);
// configure internal provider
internalProvider.setDocumentWrapper(createTransaction());
final PipedInputStream pIn = new PipedInputStream();
PipedOutputStream pOut = new PipedOutputStream(pIn);
currentExecuteStream = pOut;
Future<Response> futureResponse = null;
IOReporter reporter = createReporter();
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
// read the stream (in another thread)
futureResponse = executor.submit(new Callable<Response>() {
@Override
public Response call() throws Exception {
Proxy proxy = ProxyUtil.findProxy(targetWfs.getLocation());
Request request = Request.Post(targetWfs.getLocation()).bodyStream(pIn,
ContentType.APPLICATION_XML);
Executor executor = FluentProxyUtil.setProxy(request, proxy);
// authentication
String user = getParameter(PARAM_USER).as(String.class);
String password = getParameter(PARAM_PASSWORD).as(String.class);
if (user != null) {
// target host
int port = targetWfs.getLocation().getPort();
String hostName = targetWfs.getLocation().getHost();
String scheme = targetWfs.getLocation().getScheme();
HttpHost host = new HttpHost(hostName, port, scheme);
// add credentials
Credentials cred = ClientProxyUtil.createCredentials(user, password);
executor.auth(new AuthScope(host), cred);
executor.authPreemptive(host);
}
try {
return executor.execute(request);
} finally {
pIn.close();
}
}
});
// write the stream
SubtaskProgressIndicator subprogress = new SubtaskProgressIndicator(progress);
reporter = (IOReporter) super.execute(subprogress);
} finally {
executor.shutdown();
}
try {
Response response = futureResponse.get();
HttpResponse res = response.returnResponse();
int statusCode = res.getStatusLine().getStatusCode();
XPathFactory xPathfactory = XPathFactory.newInstance();
XPath xpath = xPathfactory.newXPath();
if (statusCode >= 200 && statusCode < 300) {
// success
reporter.setSuccess(reporter.isSuccess());
// construct summary from response
try {
Document responseDoc = parseResponse(res.getEntity());
// totalInserted
String inserted = xpath.compile("//TransactionSummary/totalInserted")
.evaluate(responseDoc);
// XXX totalUpdated
// XXX totalReplaced
// XXX totalDeleted
reporter.setSummary("Inserted " + inserted + " features.");
} catch (XPathExpressionException e) {
log.error("Error in XPath used to evaluate service response");
} catch (ParserConfigurationException | SAXException e) {
reporter.error(new IOMessageImpl(MessageFormat.format(
"Server returned status code {0}, but could not parse server response",
statusCode), e));
reporter.setSuccess(false);
}
}
else {
// failure
reporter.error(new IOMessageImpl(
"Server reported failure with code " + res.getStatusLine().getStatusCode()
+ ": " + res.getStatusLine().getReasonPhrase(),
null));
reporter.setSuccess(false);
try {
Document responseDoc = parseResponse(res.getEntity());
String errorText = xpath.compile("//ExceptionText/text()")
.evaluate(responseDoc);
reporter.setSummary("Request failed: " + errorText);
} catch (XPathExpressionException e) {
log.error("Error in XPath used to evaluate service response");
} catch (ParserConfigurationException | SAXException e) {
reporter.error(new IOMessageImpl("Could not parse server response", e));
reporter.setSuccess(false);
}
}
} catch (ExecutionException | InterruptedException e) {
reporter.error(new IOMessageImpl("Failed to execute WFS-T request", e));
reporter.setSuccess(false);
}
progress.end();
return reporter;
}
private Document parseResponse(HttpEntity entity)
throws IOException, ParserConfigurationException, SAXException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
byte[] data = EntityUtils.toByteArray(entity);
try (InputStream response = new ByteArrayInputStream(data)) {
return builder.parse(response);
} catch (SAXException e) {
String response = new String(data, StandardCharsets.UTF_8);
throw new SAXException("Invalid XML response: " + response, e);
}
}
/**
* @return the transaction wrapper
*/
protected abstract XmlWrapper createTransaction();
}