NFConnectorImpl.java 14.7 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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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
171
172
173
174
175
176
177
178
179
180
181
182
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
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
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
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;
	}
	
	/**
	 * Returns the status of any existing nF job.
	 * 
	 * @param jobId The id of the job for which you want to request the status.
	 * @return The status of any existing nF job.
	 * @throws IOException An error occurred during the request. This could be a malformed URL or a network failure.
	 */
	@Override
	public JobStatus requestJobStatus(int jobId) {
		JobStatus result = JobStatus.UNKOWN;
		try {
			List<String> parameters = Arrays.asList(buildParameter("jobid", jobId));
			result = getJobStatusFromResponse(getResponse(buildURL(REMOTE_STATUS_SERVLET, parameters)));
		} catch (MalformedURLException ex) {
			result.setMessage(MALFORMED_URL);
		} catch (IOException ex) {
			result.setMessage(IO_EXCEPTION_OCCURRED);
		} catch (ParserConfigurationException | SAXException ex) {
			result.setMessage(XML_PARSER_ERROR);
		}
		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
	public File requestJobResult(int jobId) {
		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();
		}
		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.
	 * @return The status of the job which was encoded in the given XML string.
	 * @throws ParserConfigurationException Something went wrong.
	 * @throws SAXException Some parse error.
	 * @throws IOException Some parse error.
	 */
	private JobStatus getJobStatusFromResponse(String xml)	
			throws ParserConfigurationException, SAXException, IOException {
		JobStatus result = JobStatus.UNKOWN;
		SAXParserFactory saxFactory = SAXParserFactory.newInstance();
		SAXParser parser = saxFactory.newSAXParser();
		StringReader reader = new StringReader(xml);
		ReportHandler handler = new ReportHandler();
		parser.parse(new InputSource(reader), handler);
		if (Objects.nonNull(handler.serviceException)) {
			result.setMessage(handler.serviceException);
		} else if (Objects.nonNull(handler.statusId)) {
			result = JobStatus.getInstanceForStatus(handler.statusId);
		} 
		if (Objects.nonNull(handler.jobId)) {
			result.setJobId(handler.jobId);			
		}
		return result;
	}
	
	/**
	 * 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. 
	 * @return Returns a handle to the downloaded (zipped) CityGML file.
	 * @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
	public JobStatus sendExportJob(File file) {
		JobStatus result = JobStatus.UNKOWN;
		try {
			URL url = buildURL(REMOTE_ORDER_SERVLET, null);
			JobStatus status = sendFile(url, file);
333
			if (Objects.nonNull(status.getJobId()) && status.getJobId() > 0) {
334
				status = requestJobStatus(status.getJobId());
335
			} else if (Objects.nonNull(status.getMessage()) && status.getMessage().isEmpty()) {
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
375
376
				status.setMessage(UNKNOWN_ERROR_OCCURRED);
			}
			result = status;
		} catch (MalformedURLException ex) {
			result.setMessage(MALFORMED_URL);
		} catch (IOException ex) {
			result.setMessage(IO_EXCEPTION_OCCURRED);
		} catch (ParserConfigurationException | SAXException ex) {
			result.setMessage(XML_PARSER_ERROR);
		}
		return result;
	}
	
	/**
	 * Does the actual job for the sendExportJob() method. sendExportJob() builds the URL for the remote servlet
	 * and calls this method.
	 * 
	 * @param url The URL of the remote order servlet to which the file should be send to.
	 * @param file The file to be sent to nF.
	 * @return Returns the status of the export job you just sent.
	 * @throws IOException There was a problem with your data connection.
	 * @throws ParserConfigurationException There was a problem while parsing the response.
	 * @throws SAXException Some parse error while parsing the response.
	 */
	private JobStatus sendFile(URL url, File file) throws IOException, ParserConfigurationException, SAXException {
		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();
		return getJobStatusFromResponse(getResponse(connection));
	}
	
}