NFConnectorImpl.java 17.9 KB
Newer Older
1
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
34
35
36
37
38
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
package eu.simstadt.nf;

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
 */
public class NFConnectorImpl implements NFConnector {

	/** 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;
	}
	
	/**
128
	 * Returns the status of any existing nF export job.
129
	 * 
130
131
	 * @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.
132
133
134
	 * @throws IOException An error occurred during the request. This could be a malformed URL or a network failure.
	 */
	@Override
135
136
	public ExportJob requestExportJob(int jobId) {
		ExportJob result = ExportJob.getUnkownInstance();
137
138
		try {
			List<String> parameters = Arrays.asList(buildParameter("jobid", jobId));
139
			getJobFromResponse(result, getResponse(buildURL(REMOTE_STATUS_SERVLET, parameters)));
140
		} catch (MalformedURLException ex) {
141
			result.getStatus().setMessage(MALFORMED_URL);
142
		} catch (IOException ex) {
143
			result.getStatus().setMessage(IO_EXCEPTION_OCCURRED);
144
		} catch (ParserConfigurationException | SAXException ex) {
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
			result.getStatus().setMessage(XML_PARSER_ERROR);
		}
		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 ImportJob requestImportJob(int jobId) {
		ImportJob result = ImportJob.getUnkownInstance();
		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);
		} catch (IOException ex) {
			result.getStatus().setMessage(IO_EXCEPTION_OCCURRED);
		} catch (ParserConfigurationException | SAXException ex) {
			result.getStatus().setMessage(XML_PARSER_ERROR);
171
172
173
174
175
176
177
178
179
180
181
		}
		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
182
	public File requestExportJobResult(int jobId) {
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
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
		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) {
			ex.printStackTrace();
		} catch (IOException ex) {
			ex.printStackTrace();
		}
		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();
		}
274
275
276
277
278
		
		
		System.out.println(xml);
		
		
279
280
281
282
283
284
285
286
287
		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.
288
289
	 * @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. 
290
291
292
293
	 * @throws ParserConfigurationException Something went wrong.
	 * @throws SAXException Some parse error.
	 * @throws IOException Some parse error.
	 */
294
	private void getJobFromResponse(Job job, String xml)
295
			throws ParserConfigurationException, SAXException, IOException {
296
297
		JobStatus status = JobStatus.UNKOWN;
		job.setStatus(status);
298
299
300
301
302
		SAXParserFactory saxFactory = SAXParserFactory.newInstance();
		SAXParser parser = saxFactory.newSAXParser();
		StringReader reader = new StringReader(xml);
		ReportHandler handler = new ReportHandler();
		parser.parse(new InputSource(reader), handler);
303
304
		if (Objects.nonNull(handler.statusId)) {
			job.setStatusForCode(handler.statusId);
305
		} 
306
307
308
		if (Objects.nonNull(handler.serviceException)) {
			status.setMessage(handler.serviceException);
		}
309
		if (Objects.nonNull(handler.jobId)) {
310
			job.setJobId(handler.jobId);			
311
312
313
314
315
316
317
		}
	}
	
	/**
	 * 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. 
318
319
	 * @return Returns a handle to the downloaded (zipped) CityGML file. Will be null if no file has been
	 * downloaded (due to errors).
320
321
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.
	 */
	@Override
361
362
	public ExportJob sendExportJobFile(File file) {
		ExportJob result = ExportJob.getUnkownInstance();
363
364
		try {
			URL url = buildURL(REMOTE_ORDER_SERVLET, null);
365
366
367
368
369
370
371
372
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);
			Files.copy(file.toPath(), os);
			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());
382
383
			}
		} catch (MalformedURLException ex) {
384
			result.getStatus().setMessage(MALFORMED_URL);
385
		} catch (IOException ex) {
386
			result.getStatus().setMessage(IO_EXCEPTION_OCCURRED);
387
		} catch (ParserConfigurationException | SAXException ex) {
388
			result.getStatus().setMessage(XML_PARSER_ERROR);
389
390
391
		}
		return result;
	}
392

393
	/**
394
395
	 * 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:
396
	 * 
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
	 * 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
	 *     
	 * 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.
418
	 */
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
	@Override
	public ImportJob sendImportJobFile(File importJobFile) {
		ImportJob result = ImportJob.getUnkownInstance();
		try {
			List<String> parameters = Arrays.asList(
					buildParameter("request", "imp"),
					buildParameter("pdctKrz", "LBTEST"),
					buildParameter("createjob", "1"));
			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);
		} catch (IOException ex) {
			result.getStatus().setMessage(IO_EXCEPTION_OCCURRED);
		} catch (SAXException | ParserConfigurationException ex) {
			result.getStatus().setMessage(XML_PARSER_ERROR);
		}
		return result;
450
451
452
	}
	
}