NFConnectorImpl.java 19.2 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 {
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
	public ExportJob requestExportJob(int jobId) throws FailedTransmissionException {
139
		ExportJob result = new ExportJob(jobId, 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
	public ImportJob requestImportJob(int jobId) throws FailedTransmissionException {
165
		ImportJob result = new ImportJob(jobId, 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
300
301
302
303
304
			throws ParserConfigurationException, SAXException, IOException {
		SAXParserFactory saxFactory = SAXParserFactory.newInstance();
		SAXParser parser = saxFactory.newSAXParser();
		StringReader reader = new StringReader(xml);
		ReportHandler handler = new ReportHandler();
		parser.parse(new InputSource(reader), handler);
305
306
		if (Objects.nonNull(handler.statusId)) {
			job.setStatusForCode(handler.statusId);
307
		}
308
		if (Objects.nonNull(handler.serviceException)) {
309
			job.getStatus().setMessage(handler.serviceException);
310
		}
311
		if (Objects.nonNull(handler.jobId)) {
312
			job.setId(handler.jobId);
313
314
315
316
317
318
319
		}
	}
	
	/**
	 * 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. 
320
321
	 * @return Returns a handle to the downloaded (zipped) CityGML file. Will be null if no file has been
	 * downloaded (due to errors).
322
323
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
	 * @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.
361
	 * @throws InvalidJobDescriptorException 
362
363
	 */
	@Override
364
	public void sendAndUpdateExportJob(ExportJob job) 
365
366
			throws InvalidJobDescriptorException, FailedTransmissionException {
		JobFileBuilderImpl jobFileBuilder = new JobFileBuilderImpl();
367
		File exportJobFile = jobFileBuilder.buildExportJobFile(job.getDescriptor());
368
369
		try {
			URL url = buildURL(REMOTE_ORDER_SERVLET, null);
370
371
372
373
374
375
376
377
378
			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);
379
			Files.copy(exportJobFile.toPath(), os);
380
381
			os.flush();
			writer.append(CRLF).flush();
382
383
384
385
			getJobFromResponse(job, getResponse(connection));
			// At this line, the job status will be unknown, although the job has been enqueued by the nF.
			// A separate status request has to be sent in order to get the actual state of the job. 
			job.updateStatus();
386
		} catch (MalformedURLException ex) {
387
			job.getStatus().setMessage(MALFORMED_URL);
388
			throw new FailedTransmissionException(ex.getMessage());
389
		} catch (IOException ex) {
390
			job.getStatus().setMessage(IO_EXCEPTION_OCCURRED);
391
			throw new FailedTransmissionException(ex.getMessage());
392
		} catch (ParserConfigurationException | SAXException ex) {
393
			job.getStatus().setMessage(XML_PARSER_ERROR);
394
			throw new FailedTransmissionException(ex.getMessage());
395
396
		}
	}
397

398
	/**
399
400
	 * 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:
401
	 * 
402
403
404
405
406
	 * 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
407
	 * 
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
	 * 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.
423
	 * @throws FailedTransmissionException 
424
	 */
425
	@Override
426
	public void sendAndUpdateImportJob(ImportJob job) 
427
428
			throws InvalidJobDescriptorException, FailedTransmissionException {
		JobFileBuilderImpl jobFileBuilder = new JobFileBuilderImpl();
429
		File importJobFile = jobFileBuilder.buildImportJobFile(job.getDescriptor());
430
431
		try {
			List<String> parameters = Arrays.asList(
432
433
434
					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
435
436
437
438
439
440
441
442
443
444
445
446
447
448
			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();
449
			getJobFromResponse(job, getResponse(connection));
450
		} catch (MalformedURLException ex) {
451
			job.getStatus().setMessage(MALFORMED_URL);
452
			throw new FailedTransmissionException(ex.getMessage());
453
		} catch (IOException ex) {
454
			job.getStatus().setMessage(IO_EXCEPTION_OCCURRED);
455
			throw new FailedTransmissionException(ex.getMessage());
456
		} catch (SAXException | ParserConfigurationException ex) {
457
			job.getStatus().setMessage(XML_PARSER_ERROR);
458
			throw new FailedTransmissionException(ex.getMessage());
459
		}
460
461
462
	}
	
}