HTTPConnection.java 20.3 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
27
28
29

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;

bruse's avatar
bruse committed
30
31
32
33
34
35
import eu.simstadt.nf4j.Job;
import eu.simstadt.nf4j.FailedTransmissionException;
import eu.simstadt.nf4j.InvalidJobDescriptorException;
import eu.simstadt.nf4j.JobStatus;
import eu.simstadt.nf4j.Connector;

36
37
38
39
/**
 * 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.
 * 
bruse's avatar
bruse committed
40
41
42
 * 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. 
 * 
43
 * @author Marcel Bruse
44
45
46
 * 
 * @param <I> The import job descriptor implementation for this connector.
 * @param <E> The export job descriptor implementation for this connector.
47
 */
bruse's avatar
bruse committed
48
public class HTTPConnection implements Connector<AsyncImportJob, AsyncExportJob> {
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102

	/** 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;
	
	/**
103
	 * Constructs your Connector instance.
104
105
106
	 * 
	 * @param server The host name of the nF server with which you want to establish a data connection.
	 */
bruse's avatar
bruse committed
107
	public HTTPConnection(String server) {
108
109
110
111
		this(server, DEFAULT_PORT, DEFAULT_CONTEXT, DEFAULT_PROTOCOL);
	}
	
	/**
112
	 * Constructs your Connector instance.
113
114
115
116
117
	 * 
	 * @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
118
	public HTTPConnection(String server, int port, String context, String protocol) {
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
		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;
	}
	
	/**
141
	 * Returns the status of any existing nF export job.
142
	 * 
143
144
	 * @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.
145
146
147
	 * @throws IOException An error occurred during the request. This could be a malformed URL or a network failure.
	 */
	@Override
bruse's avatar
bruse committed
148
149
	public AsyncExportJob requestExportJob(int jobId) throws FailedTransmissionException {
		AsyncExportJob result = new AsyncExportJob(jobId, this);
150
151
		try {
			List<String> parameters = Arrays.asList(buildParameter("jobid", jobId));
152
			getJobFromResponse(result, getResponse(buildURL(REMOTE_STATUS_SERVLET, parameters)));
153
		} catch (MalformedURLException ex) {
154
			result.getStatus().setMessage(MALFORMED_URL);
155
			throw new FailedTransmissionException(ex.getMessage());
156
		} catch (IOException ex) {
157
			result.getStatus().setMessage(IO_EXCEPTION_OCCURRED);
158
			throw new FailedTransmissionException(ex.getMessage());
159
		} catch (ParserConfigurationException | SAXException ex) {
160
			result.getStatus().setMessage(XML_PARSER_ERROR);
161
			throw new FailedTransmissionException(ex.getMessage());
162
163
164
165
166
167
168
169
170
171
172
173
		}
		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
bruse's avatar
bruse committed
174
175
	public AsyncImportJob requestImportJob(int jobId) throws FailedTransmissionException {
		AsyncImportJob result = new AsyncImportJob(jobId, this);
176
177
178
179
180
181
182
		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);
183
			throw new FailedTransmissionException(ex.getMessage());
184
185
		} catch (IOException ex) {
			result.getStatus().setMessage(IO_EXCEPTION_OCCURRED);
186
			throw new FailedTransmissionException(ex.getMessage());
187
188
		} catch (ParserConfigurationException | SAXException ex) {
			result.getStatus().setMessage(XML_PARSER_ERROR);
189
			throw new FailedTransmissionException(ex.getMessage());
190
191
192
193
194
195
196
197
198
199
200
		}
		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
bruse's avatar
bruse committed
201
202
203
204
	public File requestExportJobResult(AsyncExportJob job) throws FailedTransmissionException {
		if (!job.hasFinished()) {
			throw new FailedTransmissionException("Job is not finished yet!");
		}
205
206
207
208
209
		File result = null;
		try {
			List<String> parameters = Arrays.asList(
					buildParameter("request", "downloadJob"),
					buildParameter("mode", 0),
bruse's avatar
bruse committed
210
					buildParameter("jobId", job.getId()));
211
212
			result = downloadFile(buildURL(REMOTE_ORDER_SERVLET, parameters));
		} catch (MalformedURLException ex) {
213
			throw new FailedTransmissionException(ex.getMessage());
214
		} catch (IOException ex) {
215
			throw new FailedTransmissionException(ex.getMessage());
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
		}
		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<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);
	}
	
	/**
	 * 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
301
302
303
	 * 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.
304
305
	 * 
	 * @param xml A XML string that is supposed to be a XML document.
306
307
	 * @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. 
308
309
310
311
	 * @throws ParserConfigurationException Something went wrong.
	 * @throws SAXException Some parse error.
	 * @throws IOException Some parse error.
	 */
bruse's avatar
bruse committed
312
313
	private void getJobFromResponse(Job job, String xml)
			throws ParserConfigurationException, SAXException, IOException, FailedTransmissionException {		
314
315
316
317
318
		SAXParserFactory saxFactory = SAXParserFactory.newInstance();
		SAXParser parser = saxFactory.newSAXParser();
		StringReader reader = new StringReader(xml);
		ReportHandler handler = new ReportHandler();
		parser.parse(new InputSource(reader), handler);
319
320
		if (Objects.nonNull(handler.statusId)) {
			job.setStatusForCode(handler.statusId);
321
		}
322
		if (Objects.nonNull(handler.jobId)) {
323
			job.setId(handler.jobId);
324
		}
bruse's avatar
bruse committed
325
326
327
		if (Objects.nonNull(handler.serviceException)) {
			throw new FailedTransmissionException(handler.serviceException);
		}
328
329
330
331
332
333
	}
	
	/**
	 * 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. 
334
335
	 * @return Returns a handle to the downloaded (zipped) CityGML file. Will be null if no file has been
	 * downloaded (due to errors).
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
	 * @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();
            FileOutputStream outputStream = new FileOutputStream(filename);
            int bytesRead = -1;
            byte[] buffer = new byte[4096];
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
            outputStream.close();
            inputStream.close();
            handle = new File(filename);
		} 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.
375
	 * @throws InvalidJobDescriptorException 
376
377
	 */
	@Override
bruse's avatar
bruse committed
378
	public void sendAndUpdateExportJob(AsyncExportJob job) 
379
380
			throws InvalidJobDescriptorException, FailedTransmissionException {
		JobFileBuilderImpl jobFileBuilder = new JobFileBuilderImpl();
381
		File exportJobFile = jobFileBuilder.buildExportJobFile(job.getDescriptor());
382
383
		try {
			URL url = buildURL(REMOTE_ORDER_SERVLET, null);
384
385
386
387
388
389
390
391
392
			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);
393
			Files.copy(exportJobFile.toPath(), os);
394
395
			os.flush();
			writer.append(CRLF).flush();
396
			getJobFromResponse(job, getResponse(connection));
bruse's avatar
bruse committed
397
398
399
400
401
			if (job.getId() > 0) {
				job.setStatus(JobStatus.SENT, Optional.empty());
			} else {
				throw new FailedTransmissionException("Job didn't receive an id from the nF server.");
			}
402
		} catch (MalformedURLException ex) {
403
			job.getStatus().setMessage(MALFORMED_URL);
404
			throw new FailedTransmissionException(ex.getMessage());
405
		} catch (IOException ex) {
406
			job.getStatus().setMessage(IO_EXCEPTION_OCCURRED);
407
			throw new FailedTransmissionException(ex.getMessage());
408
		} catch (ParserConfigurationException | SAXException ex) {
409
			job.getStatus().setMessage(XML_PARSER_ERROR);
410
			throw new FailedTransmissionException(ex.getMessage());
411
412
		}
	}
413

414
	/**
415
416
	 * 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:
417
	 * 
418
419
420
421
422
	 * 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
	 * 
	 *     <Product>_<Tile>.start
423
	 * 
424
	 * The start file should contain the level to which your manipulated CityGML should be imported. Your CityGML
425
	 * file and the ZIP archive has to be named after the following naming convention:
426
427
428
429
430
431
432
433
434
435
436
437
438
	 * 
	 *     <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
	 * 
	 * @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.
439
	 * @throws FailedTransmissionException 
440
	 */
441
	@Override
bruse's avatar
bruse committed
442
	public void sendAndUpdateImportJob(AsyncImportJob job) 
443
			throws InvalidJobDescriptorException, FailedTransmissionException {
bruse's avatar
bruse committed
444
445
446
		if (job.getStatus() != JobStatus.LOCAL) {
			throw new FailedTransmissionException("Job cannot be sent twice!");
		}
447
		JobFileBuilderImpl jobFileBuilder = new JobFileBuilderImpl();
448
		File importJobFile = jobFileBuilder.buildImportJobFile(job.getDescriptor());
449
		try {
bruse's avatar
bruse committed
450
			String product = job.getDescriptor().getProduct();
451
			List<String> parameters = Arrays.asList(
452
					buildParameter("request", "imp"),    // trigger import
bruse's avatar
bruse committed
453
					buildParameter("pdctKrz", product),  // the nF product to import to
454
					buildParameter("createjob", "1"));   // force nF to create an import job with a job number
455
456
457
458
459
460
461
462
463
464
465
466
467
468
			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();
469
			getJobFromResponse(job, getResponse(connection));
470
471
472
473
474
			if (job.getId() > 0) {
				job.setStatus(JobStatus.SENT, Optional.empty());
			} else {
				throw new FailedTransmissionException("Job didn't receive an id from the nF server.");
			}
475
		} catch (MalformedURLException ex) {
476
			job.getStatus().setMessage(MALFORMED_URL);
477
			throw new FailedTransmissionException(ex.getMessage());
478
		} catch (IOException ex) {
479
			job.getStatus().setMessage(IO_EXCEPTION_OCCURRED);
480
			throw new FailedTransmissionException(ex.getMessage());
481
		} catch (SAXException | ParserConfigurationException ex) {
482
			job.getStatus().setMessage(XML_PARSER_ERROR);
483
			throw new FailedTransmissionException(ex.getMessage());
484
		}
485
486
487
	}
	
}