HTTPConnection.java 20.1 KB
Newer Older
bruse's avatar
bruse committed
1
package eu.simstadt.nf4j.async;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

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;
bruse's avatar
bruse committed
21
import java.util.Optional;
22
23
24
25
26
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;
27
import eu.simstadt.nf4j.Connector;
bruse's avatar
bruse committed
28
29
import eu.simstadt.nf4j.FailedTransmissionException;
import eu.simstadt.nf4j.InvalidJobDescriptorException;
30
import eu.simstadt.nf4j.Job;
bruse's avatar
bruse committed
31
import eu.simstadt.nf4j.JobStatus;
32

bruse's avatar
bruse committed
33

34
/**
35
36
 * 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.
37
 * 
bruse's avatar
bruse committed
38
 * Please note, that this connector doesn't act asynchronously. This connector is safe, but will block your main
39
 * application. You may rather want to use the asynchronous job implementations.
bruse's avatar
bruse committed
40
 * 
41
 * @author Marcel Bruse
42
43
44
 * 
 * @param <I> The import job descriptor implementation for this connector.
 * @param <E> The export job descriptor implementation for this connector.
45
 */
46
47
public class HTTPConnection implements Connector<AsyncImportJob, AsyncExportJob>
{
48
49
50

	/** Supported version of the novaFACTORY. */
	public static final String NOVA_FACTORY_VERSION = "6.3.1.1";
51

52
53
	/** 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";
54

55
56
	/** The default port number of the nF web application. */
	public static final int DEFAULT_PORT = 80;
57

58
59
	/** The default protocol for requests. */
	public static final String DEFAULT_PROTOCOL = "http";
60

61
62
	/** The standard char set for requests. */
	public static final String DEFAULT_CHARSET = "UTF-8";
63

64
65
	/** The standard line separator required for file transmission via multipart/form-data. */
	public static final String CRLF = "\r\n";
66

67
68
	/** The name of nF's remote order servlet. */
	public static final String REMOTE_ORDER_SERVLET = "RemoteOrder";
69

70
71
	/** The name of nF's remote status servlet. */
	public static final String REMOTE_STATUS_SERVLET = "RemoteStatus";
72

73
74
	/** The name of nF's remote import servlet. */
	public static final String REMOTE_IMPORT_SERVLET = "RemoteImport";
75

76
77
	/** Default parser error message. */
	public static final String XML_PARSER_ERROR = "Request failed due to an unknown XML parser error.";
78

79
	/** Default IO exception occurred message. */
80
	public static final String IO_EXCEPTION_OCCURRED =
81
			"Request failed due to an IO exception. Check the host and port.";
82

83
84
	/** Default malformed URL message. */
	public static final String MALFORMED_URL = "Request failed due to a malformed URL.";
85

86
87
	/** Default unknown error message. */
	public static final String UNKNOWN_ERROR_OCCURRED = "An unknown error occurred.";
88

89
90
	/** The server or host name of the nF server. */
	private String server;
91

92
93
	/** The port of the nF server. */
	private int port;
94

95
96
	/** The protocol of the data connection. */
	private String protocol;
97

98
99
	/** The context of the nF web application. It is part of any request URL directed at the nF server. */
	private String context;
100

101
	/**
102
	 * Constructs your Connector instance.
103
104
105
	 * 
	 * @param server The host name of the nF server with which you want to establish a data connection.
	 */
bruse's avatar
bruse committed
106
	public HTTPConnection(String server) {
107
108
		this(server, DEFAULT_PORT, DEFAULT_CONTEXT, DEFAULT_PROTOCOL);
	}
109

110
	/**
111
	 * Constructs your Connector instance.
112
113
114
115
116
	 * 
	 * @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.
	 */
bruse's avatar
bruse committed
117
	public HTTPConnection(String server, int port, String context, String protocol) {
118
119
120
121
122
123
124
125
126
127
		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.
	 * 
128
129
130
	 * 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.
131
132
133
134
135
136
137
	 * 
	 * @return The supported version of the novaFACTORY.
	 */
	@Override
	public String supportsNFVersion() {
		return NOVA_FACTORY_VERSION;
	}
138

139
	/**
140
	 * Returns the status of any existing nF export job.
141
	 * 
142
143
	 * @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.
144
145
146
	 * @throws IOException An error occurred during the request. This could be a malformed URL or a network failure.
	 */
	@Override
bruse's avatar
bruse committed
147
148
	public AsyncExportJob requestExportJob(int jobId) throws FailedTransmissionException {
		AsyncExportJob result = new AsyncExportJob(jobId, this);
149
150
		try {
			List<String> parameters = Arrays.asList(buildParameter("jobid", jobId));
151
			getJobFromResponse(result, getResponse(buildURL(REMOTE_STATUS_SERVLET, parameters)));
152
		} catch (MalformedURLException ex) {
153
			result.getStatus().setMessage(MALFORMED_URL);
154
			throw new FailedTransmissionException(ex.getMessage());
155
		} catch (IOException ex) {
156
			result.getStatus().setMessage(IO_EXCEPTION_OCCURRED);
157
			throw new FailedTransmissionException(ex.getMessage());
158
		} catch (ParserConfigurationException | SAXException ex) {
159
			result.getStatus().setMessage(XML_PARSER_ERROR);
160
			throw new FailedTransmissionException(ex.getMessage());
161
162
163
		}
		return result;
	}
164

165
166
167
168
169
170
171
172
	/**
	 * 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
bruse's avatar
bruse committed
173
174
	public AsyncImportJob requestImportJob(int jobId) throws FailedTransmissionException {
		AsyncImportJob result = new AsyncImportJob(jobId, this);
175
176
177
178
179
180
181
		try {
			List<String> 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);
182
			throw new FailedTransmissionException(ex.getMessage());
183
184
		} catch (IOException ex) {
			result.getStatus().setMessage(IO_EXCEPTION_OCCURRED);
185
			throw new FailedTransmissionException(ex.getMessage());
186
187
		} catch (ParserConfigurationException | SAXException ex) {
			result.getStatus().setMessage(XML_PARSER_ERROR);
188
			throw new FailedTransmissionException(ex.getMessage());
189
190
191
		}
		return result;
	}
192

193
194
195
196
	/**
	 * 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.
197
	 * @return A file handle to the result of the nF export job.
198
199
	 */
	@Override
bruse's avatar
bruse committed
200
201
202
203
	public File requestExportJobResult(AsyncExportJob job) throws FailedTransmissionException {
		if (!job.hasFinished()) {
			throw new FailedTransmissionException("Job is not finished yet!");
		}
204
205
206
207
208
		File result = null;
		try {
			List<String> parameters = Arrays.asList(
					buildParameter("request", "downloadJob"),
					buildParameter("mode", 0),
bruse's avatar
bruse committed
209
					buildParameter("jobId", job.getId()));
210
211
			result = downloadFile(buildURL(REMOTE_ORDER_SERVLET, parameters));
		} catch (MalformedURLException ex) {
212
			throw new FailedTransmissionException(ex.getMessage());
213
		} catch (IOException ex) {
214
			throw new FailedTransmissionException(ex.getMessage());
215
216
217
		}
		return result;
	}
218

219
220
221
222
223
224
225
226
227
228
	/**
	 * 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;
	}
229

230
	/**
231
	 * Builds a HTTP GET request URL out of the used protocol, nF server, port, context, servlet and existing parameters.
232
233
234
235
236
237
238
239
240
241
242
243
244
	 * 
	 * @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<String> 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);
	}
245

246
247
248
	/**
	 * Requests a response from the given URL. The response is expected to be returned as nF XML report.
	 * 
249
250
	 * @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.
251
	 * @return The response is expected to be a XML report, which will then be returned as a String.
252
253
	 * @throws IOException You will get some of this, if anything goes wrong with your data connection to your nF
	 *            instance.
254
255
256
257
258
259
	 */
	private String getResponse(URL url) throws UnsupportedEncodingException, IOException {
		HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
		httpConnection.setRequestProperty("Accept-Charset", DEFAULT_CHARSET);
		return getResponse(httpConnection);
	}
260

261
	/**
262
	 * Requests a response from the given HTTP connection. The response is expected to be a XML document.
263
	 * 
264
265
	 * @param httpConnection The HTTP Connection to the nF servlet. Read the nF handbook for more details about the usage
	 *           of the nF servlets.
266
267
	 * @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.
268
269
	 * @throws IOException You will get some of this, if anything goes wrong with your data connection to your nF
	 *            instance.
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
	 */
	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;
288
				}
289
290
291
292
293
294
295
			}
			response.close();
		} else {
			throw new IOException();
		}
		return xml;
	}
296

297
	/**
298
299
300
301
	 * 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.
302
303
	 * 
	 * @param xml A XML string that is supposed to be a XML document.
304
305
	 * @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.
306
307
308
309
	 * @throws ParserConfigurationException Something went wrong.
	 * @throws SAXException Some parse error.
	 * @throws IOException Some parse error.
	 */
bruse's avatar
bruse committed
310
	private void getJobFromResponse(Job job, String xml)
311
			throws ParserConfigurationException, SAXException, IOException, FailedTransmissionException {
312
313
314
315
316
		SAXParserFactory saxFactory = SAXParserFactory.newInstance();
		SAXParser parser = saxFactory.newSAXParser();
		StringReader reader = new StringReader(xml);
		ReportHandler handler = new ReportHandler();
		parser.parse(new InputSource(reader), handler);
317
318
		if (Objects.nonNull(handler.statusId)) {
			job.setStatusForCode(handler.statusId);
319
		}
320
		if (Objects.nonNull(handler.jobId)) {
321
			job.setId(handler.jobId);
322
		}
bruse's avatar
bruse committed
323
324
325
		if (Objects.nonNull(handler.serviceException)) {
			throw new FailedTransmissionException(handler.serviceException);
		}
326
	}
327

328
329
	/**
	 * Downloads a (zipped) CityGML file from the nF server. The file will be the result of an export job.
330
331
332
333
	 * 
	 * @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).
334
335
336
337
338
339
340
341
	 * @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";
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
			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();
362
363
364
365
366
367
368
369
370
371
372
		} 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.
373
	 * @throws InvalidJobDescriptorException
374
375
	 */
	@Override
376
	public void sendAndUpdateExportJob(AsyncExportJob job)
377
378
			throws InvalidJobDescriptorException, FailedTransmissionException {
		JobFileBuilderImpl jobFileBuilder = new JobFileBuilderImpl();
379
		File exportJobFile = jobFileBuilder.buildExportJobFile(job.getDescriptor());
380
381
		try {
			URL url = buildURL(REMOTE_ORDER_SERVLET, null);
382
383
384
385
386
			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
387
			connection.setDoInput(true); // Expects XML response 
388
389
390
			connection.connect();
			OutputStream os = connection.getOutputStream();
			PrintWriter writer = new PrintWriter(new OutputStreamWriter(os, DEFAULT_CHARSET), true);
391
			Files.copy(exportJobFile.toPath(), os);
392
393
			os.flush();
			writer.append(CRLF).flush();
394
			getJobFromResponse(job, getResponse(connection));
bruse's avatar
bruse committed
395
396
397
398
399
			if (job.getId() > 0) {
				job.setStatus(JobStatus.SENT, Optional.empty());
			} else {
				throw new FailedTransmissionException("Job didn't receive an id from the nF server.");
			}
400
		} catch (MalformedURLException ex) {
401
			job.getStatus().setMessage(MALFORMED_URL);
402
			throw new FailedTransmissionException(ex.getMessage());
403
		} catch (IOException ex) {
404
			job.getStatus().setMessage(IO_EXCEPTION_OCCURRED);
405
			throw new FailedTransmissionException(ex.getMessage());
406
		} catch (ParserConfigurationException | SAXException ex) {
407
			job.getStatus().setMessage(XML_PARSER_ERROR);
408
			throw new FailedTransmissionException(ex.getMessage());
409
410
		}
	}
411

412
	/**
413
414
	 * 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:
415
	 * 
416
417
418
	 * 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
419
	 * 
420
	 * <Product>_<Tile>.start
421
	 * 
422
423
	 * 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:
424
	 * 
425
426
427
428
429
	 * <Product>_<Tile>_<Level>_<Operation>.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
430
431
432
	 * 
	 * @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.
433
	 * @throws FailedTransmissionException
434
	 */
435
	@Override
436
	public void sendAndUpdateImportJob(AsyncImportJob job)
437
			throws InvalidJobDescriptorException, FailedTransmissionException {
bruse's avatar
bruse committed
438
439
440
		if (job.getStatus() != JobStatus.LOCAL) {
			throw new FailedTransmissionException("Job cannot be sent twice!");
		}
441
		JobFileBuilderImpl jobFileBuilder = new JobFileBuilderImpl();
442
		File importJobFile = jobFileBuilder.buildImportJobFile(job.getDescriptor());
443
		try {
bruse's avatar
bruse committed
444
			String product = job.getDescriptor().getProduct();
445
			List<String> parameters = Arrays.asList(
446
447
448
					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
449
450
451
452
453
454
455
			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
456
			connection.setDoInput(true); // Expects XML response
457
458
459
460
461
462
			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();
463
			getJobFromResponse(job, getResponse(connection));
464
465
466
467
468
			if (job.getId() > 0) {
				job.setStatus(JobStatus.SENT, Optional.empty());
			} else {
				throw new FailedTransmissionException("Job didn't receive an id from the nF server.");
			}
469
		} catch (MalformedURLException ex) {
470
			job.getStatus().setMessage(MALFORMED_URL);
471
			throw new FailedTransmissionException(ex.getMessage());
472
		} catch (IOException ex) {
473
			job.getStatus().setMessage(IO_EXCEPTION_OCCURRED);
474
			throw new FailedTransmissionException(ex.getMessage());
475
		} catch (SAXException | ParserConfigurationException ex) {
476
			job.getStatus().setMessage(XML_PARSER_ERROR);
477
			throw new FailedTransmissionException(ex.getMessage());
478
		}
479
	}
480

481
}