package eu.simstadt.nf4j.async; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.StringReader; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Files; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Optional; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import eu.simstadt.nf4j.Connector; import eu.simstadt.nf4j.FailedTransmissionException; import eu.simstadt.nf4j.InvalidJobDescriptorException; import eu.simstadt.nf4j.Job; import eu.simstadt.nf4j.JobStatus; /** * NFConnector lets you communicate with your novaFACTORY (nF) server instance. It supports nF version 6.3.1.1. For more * technical details about the NFConnector interface @see NFConnector. * * Please note, that this connector doesn't act asynchronously. This connector is safe, but will block your main * application. You may rather want to use the asynchronous job implementations. * * @author Marcel Bruse * * @param The import job descriptor implementation for this connector. * @param The export job descriptor implementation for this connector. */ public class HTTPConnection implements Connector { /** Supported version of the novaFACTORY. */ public static final String NOVA_FACTORY_VERSION = "6.3.1.1"; /** The default context of the nF web application. It is part of any request URL directed at the nF server. */ public static final String DEFAULT_CONTEXT = "novaFACTORY"; /** The default port number of the nF web application. */ public static final int DEFAULT_PORT = 80; /** The default protocol for requests. */ public static final String DEFAULT_PROTOCOL = "http"; /** The standard char set for requests. */ public static final String DEFAULT_CHARSET = "UTF-8"; /** The standard line separator required for file transmission via multipart/form-data. */ public static final String CRLF = "\r\n"; /** The name of nF's remote order servlet. */ public static final String REMOTE_ORDER_SERVLET = "RemoteOrder"; /** The name of nF's remote status servlet. */ public static final String REMOTE_STATUS_SERVLET = "RemoteStatus"; /** The name of nF's remote import servlet. */ public static final String REMOTE_IMPORT_SERVLET = "RemoteImport"; /** Default parser error message. */ public static final String XML_PARSER_ERROR = "Request failed due to an unknown XML parser error."; /** Default IO exception occurred message. */ public static final String IO_EXCEPTION_OCCURRED = "Request failed due to an IO exception. Check the host and port."; /** Default malformed URL message. */ public static final String MALFORMED_URL = "Request failed due to a malformed URL."; /** Default unknown error message. */ public static final String UNKNOWN_ERROR_OCCURRED = "An unknown error occurred."; /** The server or host name of the nF server. */ private String server; /** The port of the nF server. */ private int port; /** The protocol of the data connection. */ private String protocol; /** The context of the nF web application. It is part of any request URL directed at the nF server. */ private String context; /** * Constructs your Connector instance. * * @param server The host name of the nF server with which you want to establish a data connection. */ public HTTPConnection(String server) { this(server, DEFAULT_PORT, DEFAULT_CONTEXT, DEFAULT_PROTOCOL); } /** * Constructs your Connector instance. * * @param server The host name of the nF server with which you want to establish a data connection. * @param port The port of the nF web application. * @param context The context of the nF web application. It is part of any request URL directed at the nF server. */ public HTTPConnection(String server, int port, String context, String protocol) { this.server = server; this.port = port; this.context = context; this.protocol = protocol; } /** * Callers of this NFConnector want to know the actual version of the novaFACTORY and the versions of its * HTTP/FTP/WPS/etc. interfaces against which this interface has been implemented. * * The NovaFACTORY interfaces may change over time. Such changes force the SimStadt programmers to implement * different versions of this NFConnector interface while keeping old implementations in order to ensure backward * compatibility. * * @return The supported version of the novaFACTORY. */ @Override public String supportsNFVersion() { return NOVA_FACTORY_VERSION; } /** * Returns the status of any existing nF export job. * * @param jobId The id of the export job for which you want to request the status. * @return The status of any existing nF export job. * @throws IOException An error occurred during the request. This could be a malformed URL or a network failure. */ @Override public AsyncExportJob requestExportJob(int jobId) throws FailedTransmissionException { AsyncExportJob result = new AsyncExportJob(jobId, this); try { List parameters = Arrays.asList(buildParameter("jobid", jobId)); getJobFromResponse(result, getResponse(buildURL(REMOTE_STATUS_SERVLET, parameters))); } catch (MalformedURLException ex) { result.getStatus().setMessage(MALFORMED_URL); throw new FailedTransmissionException(ex.getMessage()); } catch (IOException ex) { result.getStatus().setMessage(IO_EXCEPTION_OCCURRED); throw new FailedTransmissionException(ex.getMessage()); } catch (ParserConfigurationException | SAXException ex) { result.getStatus().setMessage(XML_PARSER_ERROR); throw new FailedTransmissionException(ex.getMessage()); } return result; } /** * Returns the status of any existing nF import job. * * @param jobId The id of the import job for which you want to request the status. * @return The status of any existing nF import job. * @throws IOException An error occurred during the request. This could be a malformed URL or a network failure. */ @Override public AsyncImportJob requestImportJob(int jobId) throws FailedTransmissionException { AsyncImportJob result = new AsyncImportJob(jobId, this); try { List parameters = Arrays.asList( buildParameter("jobid", jobId), buildParameter("request", "status")); getJobFromResponse(result, getResponse(buildURL(REMOTE_IMPORT_SERVLET, parameters))); } catch (MalformedURLException ex) { result.getStatus().setMessage(MALFORMED_URL); throw new FailedTransmissionException(ex.getMessage()); } catch (IOException ex) { result.getStatus().setMessage(IO_EXCEPTION_OCCURRED); throw new FailedTransmissionException(ex.getMessage()); } catch (ParserConfigurationException | SAXException ex) { result.getStatus().setMessage(XML_PARSER_ERROR); throw new FailedTransmissionException(ex.getMessage()); } return result; } /** * Downloads the result for a given nF export job and hands over the corresponding file handle. * * @param jobId The id of the export job for which the result should be loaded. * @return A file handle to the result of the nF export job. */ @Override public File requestExportJobResult(AsyncExportJob job) throws FailedTransmissionException { if (!job.hasFinished()) { throw new FailedTransmissionException("Job is not finished yet!"); } File result = null; try { List parameters = Arrays.asList( buildParameter("request", "downloadJob"), buildParameter("mode", 0), buildParameter("jobId", job.getId())); result = downloadFile(buildURL(REMOTE_ORDER_SERVLET, parameters)); } catch (MalformedURLException ex) { throw new FailedTransmissionException(ex.getMessage()); } catch (IOException ex) { throw new FailedTransmissionException(ex.getMessage()); } return result; } /** * Builds a simple parameter string for a HTTP GET request string. * * @param key The parameter name. * @param value The value of the parameter. * @return The HTTP GET request parameter as concatenated key and value. */ private String buildParameter(String key, Object value) { return key + "=" + value; } /** * Builds a HTTP GET request URL out of the used protocol, nF server, port, context, servlet and existing parameters. * * @param servlet One of the supported nF servlets RemoteOrder, RemoteStatus or RemoteImport. * @param parameters List of parameters to be send to the servlet. * @return The built URL instance. * @throws MalformedURLException You will get some of this, if you mess up any part of the URL. */ private URL buildURL(String servlet, List parameters) throws MalformedURLException { String url = String.format("%s://%s:%s/%s/%s", protocol, server, port, context, servlet); if (Objects.nonNull(parameters) && !parameters.isEmpty()) { url += "?" + String.join("&", parameters); } return new URL(url); } /** * Requests a response from the given URL. The response is expected to be returned as nF XML report. * * @param url The URL of the nF servlet with all its necessary parameters. Read the nF handbook for more details * about the usage of nF servlets. * @return The response is expected to be a XML report, which will then be returned as a String. * @throws IOException You will get some of this, if anything goes wrong with your data connection to your nF * instance. */ private String getResponse(URL url) throws UnsupportedEncodingException, IOException { HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection(); httpConnection.setRequestProperty("Accept-Charset", DEFAULT_CHARSET); return getResponse(httpConnection); } /** * Requests a response from the given HTTP connection. The response is expected to be a XML document. * * @param httpConnection The HTTP Connection to the nF servlet. Read the nF handbook for more details about the usage * of the nF servlets. * @return The response is expected to be a XML report, which will then be returned as a String. * @throws UnsupportedEncodingException You will get some of this, if your connection uses wrong encodings. * @throws IOException You will get some of this, if anything goes wrong with your data connection to your nF * instance. */ private String getResponse(HttpURLConnection httpConnection) throws UnsupportedEncodingException, IOException { String xml = ""; if (httpConnection.getResponseCode() == HttpURLConnection.HTTP_OK) { String contentType = httpConnection.getHeaderField("Content-Type"); String charset = null; for (String param : contentType.replace(" ", "").split(";")) { if (param.startsWith("charset=")) { charset = param.split("=", 2)[1]; break; } } InputStream response = httpConnection.getInputStream(); if (Objects.nonNull(charset)) { BufferedReader reader = new BufferedReader(new InputStreamReader(response, charset)); String line; while ((line = reader.readLine()) != null) { xml += line; } } response.close(); } else { throw new IOException(); } return xml; } /** * If everything works as expected, then nF's servlets will respond with XML reports to your requests. The job id and * status will be extracted from the response string. The id of the passed job instance will be updated. The XML * reports have a certain structure. Read the nF manual for more information about the XML reports and have a look at * the DTD of the XML reports. * * @param xml A XML string that is supposed to be a XML document. * @param An empty job status object about to be filled with the actual result. It will be UNKOWN if there was no XML * report in the response. * @throws ParserConfigurationException Something went wrong. * @throws SAXException Some parse error. * @throws IOException Some parse error. */ private void getJobFromResponse(Job job, String xml) throws ParserConfigurationException, SAXException, IOException, FailedTransmissionException { SAXParserFactory saxFactory = SAXParserFactory.newInstance(); SAXParser parser = saxFactory.newSAXParser(); StringReader reader = new StringReader(xml); ReportHandler handler = new ReportHandler(); parser.parse(new InputSource(reader), handler); if (Objects.nonNull(handler.statusId)) { job.setStatusForCode(handler.statusId); } if (Objects.nonNull(handler.jobId)) { job.setId(handler.jobId); } if (Objects.nonNull(handler.serviceException)) { throw new FailedTransmissionException(handler.serviceException); } } /** * Downloads a (zipped) CityGML file from the nF server. The file will be the result of an export job. * * @param url The URL of the remote order servlet that points at the CityGML file on the nF server. * @return Returns a handle to the downloaded (zipped) CityGML file. Will be null if no file has been downloaded (due * to errors). * @throws IOException Something went wrong with your data connection. Check your server and the port. */ private File downloadFile(URL url) throws IOException { File handle = null; HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection(); httpConnection.setRequestProperty("Accept-Charset", DEFAULT_CHARSET); if (httpConnection.getResponseCode() == HttpURLConnection.HTTP_OK) { String filename = "result.gml"; String disposition = httpConnection.getHeaderField("Content-Disposition"); if (disposition != null) { int index = disposition.indexOf("filename="); if (index > -1) { filename = disposition. substring(index + 9, disposition.length()). replaceAll("\"", ""). replaceAll(";", ""); } } InputStream inputStream = httpConnection.getInputStream(); handle = Files.createTempDirectory("nfDownload").resolve(filename).toFile(); FileOutputStream outputStream = new FileOutputStream(handle); int bytesRead = -1; byte[] buffer = new byte[4096]; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } outputStream.close(); inputStream.close(); } else { throw new IOException(); } return handle; } /** * Sends an export job to the nF. This job will be enqueued in the queue of export jobs. * * @param file The XML file which describes the nF export job. * @return Returns the status of the export job you just sent. * @throws InvalidJobDescriptorException */ @Override public void sendAndUpdateExportJob(AsyncExportJob job) throws InvalidJobDescriptorException, FailedTransmissionException { JobFileBuilderImpl jobFileBuilder = new JobFileBuilderImpl(); File exportJobFile = jobFileBuilder.buildExportJobFile(job.getDescriptor()); try { URL url = buildURL(REMOTE_ORDER_SERVLET, null); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("POST"); connection.setRequestProperty("Connection", "Keep-Alive"); connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); connection.setDoOutput(true); // Sends export job file connection.setDoInput(true); // Expects XML response connection.connect(); OutputStream os = connection.getOutputStream(); PrintWriter writer = new PrintWriter(new OutputStreamWriter(os, DEFAULT_CHARSET), true); Files.copy(exportJobFile.toPath(), os); os.flush(); writer.append(CRLF).flush(); getJobFromResponse(job, getResponse(connection)); if (job.getId() > 0) { job.setStatus(JobStatus.SENT, Optional.empty()); } else { throw new FailedTransmissionException("Job didn't receive an id from the nF server."); } } catch (MalformedURLException ex) { job.getStatus().setMessage(MALFORMED_URL); throw new FailedTransmissionException(ex.getMessage()); } catch (IOException ex) { job.getStatus().setMessage(IO_EXCEPTION_OCCURRED); throw new FailedTransmissionException(ex.getMessage()); } catch (ParserConfigurationException | SAXException ex) { job.getStatus().setMessage(XML_PARSER_ERROR); throw new FailedTransmissionException(ex.getMessage()); } } /** * Sends an import job to nF. The given import job file has to be a zip file and has to obey the internal structure * defined in the nF manuals. Short description: * * Import jobs enable you to add, alter and delete CityGML top level objects (like buildings) in the nF. Every import * job file has to contain a start file. Start files control the import process through their file names and their * contents. The name of a start file has the following structure * * _.start * * The start file should contain the level to which your manipulated CityGML should be imported. Your CityGML file * and the ZIP archive has to be named after the following naming convention: * * ___.gml * * Here operation can be ... - 'REP': Replaces whole existing buildings only, - 'REPUPD': Replaces whole existing * buildings and adds new buildings, - 'UPD': Update, same as REP, - 'CHG': Change, same as REPUPD, - 'DEL': Deletes * the geometry of a particular LOD, - 'DELALL': Deletes a whole building * * @param file The nF import job as a prepared ZIP file. It has to contain the CityGML file and the start file. * @return Returns the job status of your import job. * @throws FailedTransmissionException */ @Override public void sendAndUpdateImportJob(AsyncImportJob job) throws InvalidJobDescriptorException, FailedTransmissionException { if (job.getStatus() != JobStatus.LOCAL) { throw new FailedTransmissionException("Job cannot be sent twice!"); } JobFileBuilderImpl jobFileBuilder = new JobFileBuilderImpl(); File importJobFile = jobFileBuilder.buildImportJobFile(job.getDescriptor()); try { String product = job.getDescriptor().getProduct(); List parameters = Arrays.asList( buildParameter("request", "imp"), // trigger import buildParameter("pdctKrz", product), // the nF product to import to buildParameter("createjob", "1")); // force nF to create an import job with a job number URL url = buildURL(REMOTE_IMPORT_SERVLET, parameters); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("POST"); connection.setRequestProperty("Content-Type", "application/zip"); connection.setRequestProperty("Accept", "*/*"); connection.setRequestProperty("", ""); connection.setDoOutput(true); // Sends export job file connection.setDoInput(true); // Expects XML response connection.connect(); OutputStream os = connection.getOutputStream(); PrintWriter writer = new PrintWriter(new OutputStreamWriter(os, DEFAULT_CHARSET), true); Files.copy(importJobFile.toPath(), os); os.flush(); writer.append(CRLF).flush(); getJobFromResponse(job, getResponse(connection)); if (job.getId() > 0) { job.setStatus(JobStatus.SENT, Optional.empty()); } else { throw new FailedTransmissionException("Job didn't receive an id from the nF server."); } } catch (MalformedURLException ex) { job.getStatus().setMessage(MALFORMED_URL); throw new FailedTransmissionException(ex.getMessage()); } catch (IOException ex) { job.getStatus().setMessage(IO_EXCEPTION_OCCURRED); throw new FailedTransmissionException(ex.getMessage()); } catch (SAXException | ParserConfigurationException ex) { job.getStatus().setMessage(XML_PARSER_ERROR); throw new FailedTransmissionException(ex.getMessage()); } } }