NFConnectorImpl.java 19.5 KB
Newer Older
1
package eu.simstadt.nf4j;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

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 javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * 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.
 * 
 * @author Marcel Bruse
34
35
36
 * 
 * @param <I> The import job descriptor implementation for this connector.
 * @param <E> The export job descriptor implementation for this connector.
37
 */
38
public class NFConnectorImpl implements NFConnector<ImportJobDescriptorImpl, ExportJobDescriptorImpl> {
39
40
41
42
43
44
45
46
47
48
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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130

	/** 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 NFConnector instance.
	 * 
	 * @param server The host name of the nF server with which you want to establish a data connection.
	 */
	public NFConnectorImpl(String server) {
		this(server, DEFAULT_PORT, DEFAULT_CONTEXT, DEFAULT_PROTOCOL);
	}
	
	/**
	 * Constructs your NFConnector 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 NFConnectorImpl(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;
	}
	
	/**
131
	 * Returns the status of any existing nF export job.
132
	 * 
133
134
	 * @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.
135
136
137
	 * @throws IOException An error occurred during the request. This could be a malformed URL or a network failure.
	 */
	@Override
138
139
	public ExportJob requestExportJob(int jobId) throws FailedTransmissionException {
		ExportJob result = ExportJob.getNewInstance(this);
140
141
		try {
			List<String> parameters = Arrays.asList(buildParameter("jobid", jobId));
142
			getJobFromResponse(result, getResponse(buildURL(REMOTE_STATUS_SERVLET, parameters)));
143
		} catch (MalformedURLException ex) {
144
			result.getStatus().setMessage(MALFORMED_URL);
145
			throw new FailedTransmissionException(ex.getMessage());
146
		} catch (IOException ex) {
147
			result.getStatus().setMessage(IO_EXCEPTION_OCCURRED);
148
			throw new FailedTransmissionException(ex.getMessage());
149
		} catch (ParserConfigurationException | SAXException ex) {
150
			result.getStatus().setMessage(XML_PARSER_ERROR);
151
			throw new FailedTransmissionException(ex.getMessage());
152
153
154
155
156
157
158
159
160
161
162
163
		}
		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
164
165
	public ImportJob requestImportJob(int jobId) throws FailedTransmissionException {
		ImportJob result = ImportJob.getNewInstance(this);
166
167
168
169
170
171
172
		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);
173
			throw new FailedTransmissionException(ex.getMessage());
174
175
		} catch (IOException ex) {
			result.getStatus().setMessage(IO_EXCEPTION_OCCURRED);
176
			throw new FailedTransmissionException(ex.getMessage());
177
178
		} catch (ParserConfigurationException | SAXException ex) {
			result.getStatus().setMessage(XML_PARSER_ERROR);
179
			throw new FailedTransmissionException(ex.getMessage());
180
181
182
183
184
185
186
187
188
189
190
		}
		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
191
	public File requestExportJobResult(int jobId) throws FailedTransmissionException {
192
193
194
195
196
197
198
199
		File result = null;
		try {
			List<String> parameters = Arrays.asList(
					buildParameter("request", "downloadJob"),
					buildParameter("mode", 0),
					buildParameter("jobId", jobId));
			result = downloadFile(buildURL(REMOTE_ORDER_SERVLET, parameters));
		} catch (MalformedURLException ex) {
200
			throw new FailedTransmissionException(ex.getMessage());
201
		} catch (IOException ex) {
202
			throw new FailedTransmissionException(ex.getMessage());
203
204
205
206
207
208
209
210
211
212
213
214
215
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
		}
		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
	 * status will be extracted from the response string. 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.
292
293
	 * @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. 
294
295
296
297
	 * @throws ParserConfigurationException Something went wrong.
	 * @throws SAXException Some parse error.
	 * @throws IOException Some parse error.
	 */
298
	private void getJobFromResponse(Job job, String xml)
299
			throws ParserConfigurationException, SAXException, IOException {
300
301
		JobStatus status = JobStatus.UNKOWN;
		job.setStatus(status);
302
303
304
305
306
		SAXParserFactory saxFactory = SAXParserFactory.newInstance();
		SAXParser parser = saxFactory.newSAXParser();
		StringReader reader = new StringReader(xml);
		ReportHandler handler = new ReportHandler();
		parser.parse(new InputSource(reader), handler);
307
308
		if (Objects.nonNull(handler.statusId)) {
			job.setStatusForCode(handler.statusId);
309
		}
310
311
312
		if (Objects.nonNull(handler.serviceException)) {
			status.setMessage(handler.serviceException);
		}
313
		if (Objects.nonNull(handler.jobId)) {
314
			job.setJobId(handler.jobId);			
315
316
317
318
319
320
321
		}
	}
	
	/**
	 * 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. 
322
323
	 * @return Returns a handle to the downloaded (zipped) CityGML file. Will be null if no file has been
	 * downloaded (due to errors).
324
325
326
327
328
329
330
331
332
333
334
335
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
	 * @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.
363
	 * @throws InvalidJobDescriptorException 
364
365
	 */
	@Override
366
367
	public ExportJob sendExportJobFile(ExportJobDescriptorImpl jobDescriptor) 
			throws InvalidJobDescriptorException, FailedTransmissionException {
368
		ExportJob result = ExportJob.getNewInstance();
369
370
		JobFileBuilderImpl jobFileBuilder = new JobFileBuilderImpl();
		File exportJobFile = jobFileBuilder.buildExportJobFile(jobDescriptor);
371
372
		try {
			URL url = buildURL(REMOTE_ORDER_SERVLET, null);
373
374
375
376
377
378
379
380
381
			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);
382
			Files.copy(exportJobFile.toPath(), os);
383
384
385
386
387
388
389
			os.flush();
			writer.append(CRLF).flush();
			getJobFromResponse(result, getResponse(connection));
			// At this line, result will be UNKNOWN, although the job has been enqueued. A separate status request
			// will be sent in order to query for the actual state of the job. 
			if (result.getJobId() > 0) {
				result = requestExportJob(result.getJobId());
390
391
			}
		} catch (MalformedURLException ex) {
392
			result.getStatus().setMessage(MALFORMED_URL);
393
			throw new FailedTransmissionException(ex.getMessage());
394
		} catch (IOException ex) {
395
			result.getStatus().setMessage(IO_EXCEPTION_OCCURRED);
396
			throw new FailedTransmissionException(ex.getMessage());
397
		} catch (ParserConfigurationException | SAXException ex) {
398
			result.getStatus().setMessage(XML_PARSER_ERROR);
399
			throw new FailedTransmissionException(ex.getMessage());
400
401
402
		}
		return result;
	}
403

404
	/**
405
406
	 * 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:
407
	 * 
408
409
410
411
412
	 * 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
413
	 * 
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
	 * The start file should contain the level to which your manipulated CityGML should be imported. Your CityGML
	 * file has to be named after the following naming convention:
	 * 
	 *     <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.
429
	 * @throws FailedTransmissionException 
430
	 */
431
	@Override
432
433
434
435
436
	public ImportJob sendImportJobFile(ImportJobDescriptorImpl jobDescriptor) 
			throws InvalidJobDescriptorException, FailedTransmissionException {
		ImportJob result = ImportJob.getNewInstance();
		JobFileBuilderImpl jobFileBuilder = new JobFileBuilderImpl();
		File importJobFile = jobFileBuilder.buildImportJobFile(jobDescriptor);
437
438
		try {
			List<String> parameters = Arrays.asList(
439
440
441
					buildParameter("request", "imp"),    // trigger import
					buildParameter("pdctKrz", "WUDEV"),  // the nF product to import to
					buildParameter("createjob", "1"));   // force nF to create an import job with a job number
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
			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(result, getResponse(connection));
		} catch (MalformedURLException ex) {
			result.getStatus().setMessage(MALFORMED_URL);
459
			throw new FailedTransmissionException(ex.getMessage());
460
461
		} catch (IOException ex) {
			result.getStatus().setMessage(IO_EXCEPTION_OCCURRED);
462
			throw new FailedTransmissionException(ex.getMessage());
463
464
		} catch (SAXException | ParserConfigurationException ex) {
			result.getStatus().setMessage(XML_PARSER_ERROR);
465
			throw new FailedTransmissionException(ex.getMessage());
466
467
		}
		return result;
468
469
470
	}
	
}