| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 1 | package de.ids_mannheim.korap.plkexport; |
| 2 | |
| 3 | import java.io.BufferedWriter; |
| 4 | import java.io.File; |
| Akron | 1d36eb5 | 2020-11-13 17:52:26 +0100 | [diff] [blame] | 5 | import java.io.Writer; |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 6 | import java.io.FileWriter; |
| 7 | import java.io.IOException; |
| 8 | import java.io.StringWriter; |
| Akron | 55def63 | 2020-11-26 16:00:02 +0100 | [diff] [blame] | 9 | import java.io.InputStream; |
| 10 | import java.io.OutputStream; |
| 11 | import java.io.FileInputStream; |
| Akron | 1d36eb5 | 2020-11-13 17:52:26 +0100 | [diff] [blame] | 12 | |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 13 | import java.util.Collection; |
| 14 | import java.util.ArrayList; |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 15 | import java.util.Iterator; |
| 16 | import java.util.LinkedList; |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 17 | import java.util.Properties; |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 18 | |
| 19 | import com.fasterxml.jackson.core.JsonFactory; |
| 20 | import com.fasterxml.jackson.core.JsonParser; |
| 21 | import com.fasterxml.jackson.core.Version; |
| Akron | acc9f7a | 2020-11-17 17:21:40 +0100 | [diff] [blame] | 22 | import com.fasterxml.jackson.core.JsonParseException; |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 23 | import com.fasterxml.jackson.databind.JsonNode; |
| 24 | import com.fasterxml.jackson.databind.ObjectMapper; |
| 25 | |
| Akron | 1d36eb5 | 2020-11-13 17:52:26 +0100 | [diff] [blame] | 26 | import javax.ws.rs.core.Response; |
| 27 | import javax.ws.rs.core.Response.ResponseBuilder; |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 28 | import javax.ws.rs.core.Response.Status; |
| Akron | 55def63 | 2020-11-26 16:00:02 +0100 | [diff] [blame] | 29 | import javax.ws.rs.core.StreamingOutput; |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 30 | |
| Akron | af145eb | 2020-11-24 16:55:47 +0100 | [diff] [blame] | 31 | import org.glassfish.jersey.media.sse.EventOutput; |
| 32 | import org.glassfish.jersey.media.sse.OutboundEvent; |
| Akron | d0b1cfe | 2020-11-20 19:26:52 +0100 | [diff] [blame] | 33 | |
| Akron | 876017d | 2020-11-17 09:19:24 +0100 | [diff] [blame] | 34 | import static de.ids_mannheim.korap.plkexport.Util.*; |
| 35 | |
| Akron | 1d36eb5 | 2020-11-13 17:52:26 +0100 | [diff] [blame] | 36 | /** |
| 37 | * Base class for collecting matches and header information |
| 38 | * for exporters implementing the Exporter interface. |
| 39 | */ |
| Akron | 1d36eb5 | 2020-11-13 17:52:26 +0100 | [diff] [blame] | 40 | public class MatchAggregator { |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 41 | |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 42 | private final Properties prop = ExWSConf.properties(null); |
| 43 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 44 | private static final ObjectMapper mapper = new ObjectMapper(); |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 45 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 46 | // In-memory and persistant writer for data |
| Akron | 1d36eb5 | 2020-11-13 17:52:26 +0100 | [diff] [blame] | 47 | private Writer writer; |
| Akron | 1d36eb5 | 2020-11-13 17:52:26 +0100 | [diff] [blame] | 48 | private File file; |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 49 | |
| 50 | // Meta information for result exports |
| Akron | fddd058 | 2020-11-17 09:49:54 +0100 | [diff] [blame] | 51 | private JsonNode meta, query, collection; |
| Akron | 820dc64 | 2020-11-19 13:11:50 +0100 | [diff] [blame] | 52 | private String fname, queryString, corpusQueryString, src; |
| Akron | c1c1824 | 2020-11-18 18:24:12 +0100 | [diff] [blame] | 53 | private boolean timeExceeded = false; |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 54 | |
| 55 | // Result calculations (partially for progress) |
| 56 | private int totalResults = -1, |
| 57 | maxResults = -1, |
| 58 | fetchedResults = 0; |
| 59 | |
| 60 | // Event writer for progress |
| Akron | af145eb | 2020-11-24 16:55:47 +0100 | [diff] [blame] | 61 | private EventOutput evOut; |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 62 | |
| 63 | |
| 64 | /** |
| 65 | * MimeType of the exporter - |
| 66 | * defaults to "text/plain" but |
| 67 | * should be overwritten. |
| 68 | */ |
| Akron | e57937b | 2020-11-17 08:49:31 +0100 | [diff] [blame] | 69 | public String getMimeType() { |
| 70 | return "text/plain"; |
| 71 | }; |
| 72 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 73 | |
| 74 | /** |
| 75 | * Suffix of the exported file - |
| 76 | * defaults to "txt" but |
| 77 | * should be overwritten. |
| 78 | */ |
| Akron | e57937b | 2020-11-17 08:49:31 +0100 | [diff] [blame] | 79 | public String getSuffix() { |
| 80 | return "txt"; |
| 81 | }; |
| 82 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 83 | |
| 84 | /** |
| 85 | * Total results of exportable matches. |
| 86 | */ |
| Akron | c1c1824 | 2020-11-18 18:24:12 +0100 | [diff] [blame] | 87 | public int getTotalResults() { |
| 88 | return this.totalResults; |
| 89 | }; |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 90 | |
| Akron | 62d90a3 | 2020-11-18 20:45:38 +0100 | [diff] [blame] | 91 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 92 | /** |
| 93 | * Indicator if time was exceeded when |
| 94 | * fetching all matches. This means |
| 95 | * that "totalResults" needs |
| 96 | * to be treated as a minimum value. |
| 97 | */ |
| Akron | c1c1824 | 2020-11-18 18:24:12 +0100 | [diff] [blame] | 98 | public boolean hasTimeExceeded() { |
| 99 | return this.timeExceeded; |
| 100 | }; |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 101 | |
| Akron | c1c1824 | 2020-11-18 18:24:12 +0100 | [diff] [blame] | 102 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 103 | /** |
| 104 | * Set the file name of the file to |
| 105 | * be exported. |
| 106 | */ |
| Akron | 7412271 | 2020-11-17 09:41:21 +0100 | [diff] [blame] | 107 | public void setFileName (String fname) { |
| Akron | 876017d | 2020-11-17 09:19:24 +0100 | [diff] [blame] | 108 | this.fname = fname; |
| 109 | }; |
| 110 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 111 | |
| 112 | /** |
| 113 | * Get the file name of the file to |
| 114 | * be exported. |
| 115 | */ |
| Akron | 7412271 | 2020-11-17 09:41:21 +0100 | [diff] [blame] | 116 | public String getFileName () { |
| 117 | String s = this.fname; |
| 118 | if (s == null) |
| 119 | s = this.queryString; |
| 120 | if (s == null) |
| 121 | return "export"; |
| 122 | return sanitizeFileName(s); |
| Akron | 876017d | 2020-11-17 09:19:24 +0100 | [diff] [blame] | 123 | }; |
| 124 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 125 | |
| 126 | /** |
| 127 | * Set the query string. |
| 128 | */ |
| Akron | 7412271 | 2020-11-17 09:41:21 +0100 | [diff] [blame] | 129 | public void setQueryString (String query) { |
| 130 | this.queryString = query; |
| 131 | }; |
| 132 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 133 | |
| 134 | /** |
| 135 | * Get the query string. |
| 136 | */ |
| Akron | 7412271 | 2020-11-17 09:41:21 +0100 | [diff] [blame] | 137 | public String getQueryString () { |
| 138 | return this.queryString; |
| 139 | }; |
| Akron | d2072ee | 2020-11-17 16:12:41 +0100 | [diff] [blame] | 140 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 141 | |
| 142 | /** |
| 143 | * Set the corpus query string. |
| 144 | */ |
| Akron | d2072ee | 2020-11-17 16:12:41 +0100 | [diff] [blame] | 145 | public void setCorpusQueryString (String query) { |
| 146 | this.corpusQueryString = query; |
| 147 | }; |
| 148 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 149 | |
| 150 | /** |
| 151 | * Get the corpus query string. |
| 152 | */ |
| Akron | d2072ee | 2020-11-17 16:12:41 +0100 | [diff] [blame] | 153 | public String getCorpusQueryString () { |
| 154 | return this.corpusQueryString; |
| 155 | }; |
| Akron | 820dc64 | 2020-11-19 13:11:50 +0100 | [diff] [blame] | 156 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 157 | |
| 158 | /** |
| 159 | * Set the source information. |
| 160 | */ |
| Akron | 820dc64 | 2020-11-19 13:11:50 +0100 | [diff] [blame] | 161 | public void setSource (String host, String path) { |
| 162 | StringBuilder s = new StringBuilder(32); |
| 163 | if (host != null) |
| 164 | s.append(host); |
| 165 | |
| 166 | if (path != null && path.length() > 0) |
| 167 | s.append('/').append(path); |
| 168 | |
| 169 | this.src = s.toString(); |
| 170 | }; |
| 171 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 172 | |
| 173 | /** |
| 174 | * Get the source information. |
| 175 | */ |
| Akron | 820dc64 | 2020-11-19 13:11:50 +0100 | [diff] [blame] | 176 | public String getSource () { |
| 177 | return this.src; |
| 178 | }; |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 179 | |
| Akron | 7412271 | 2020-11-17 09:41:21 +0100 | [diff] [blame] | 180 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 181 | /** |
| 182 | * Set the meta JSON blob. |
| 183 | */ |
| Akron | 1d36eb5 | 2020-11-13 17:52:26 +0100 | [diff] [blame] | 184 | public void setMeta (JsonNode meta) { |
| 185 | this.meta = meta; |
| 186 | }; |
| Akron | eedac91 | 2020-11-16 12:54:42 +0100 | [diff] [blame] | 187 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 188 | |
| 189 | /** |
| 190 | * Get the meta JSON blob. |
| 191 | */ |
| Akron | eedac91 | 2020-11-16 12:54:42 +0100 | [diff] [blame] | 192 | public JsonNode getMeta () { |
| 193 | return this.meta; |
| 194 | }; |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 195 | |
| Akron | eedac91 | 2020-11-16 12:54:42 +0100 | [diff] [blame] | 196 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 197 | /** |
| 198 | * Set the query JSON blob. |
| 199 | */ |
| Akron | 1d36eb5 | 2020-11-13 17:52:26 +0100 | [diff] [blame] | 200 | public void setQuery (JsonNode query) { |
| 201 | this.query = query; |
| 202 | }; |
| 203 | |
| Akron | 62d90a3 | 2020-11-18 20:45:38 +0100 | [diff] [blame] | 204 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 205 | /** |
| 206 | * Get the query JSON blob. |
| 207 | */ |
| Akron | eedac91 | 2020-11-16 12:54:42 +0100 | [diff] [blame] | 208 | public JsonNode getQuery () { |
| 209 | return this.query; |
| 210 | }; |
| 211 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 212 | |
| 213 | /** |
| 214 | * Set the collection JSON blob. |
| 215 | */ |
| Akron | 1d36eb5 | 2020-11-13 17:52:26 +0100 | [diff] [blame] | 216 | public void setCollection (JsonNode collection) { |
| 217 | this.collection = collection; |
| 218 | }; |
| 219 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 220 | |
| 221 | /** |
| 222 | * Get the collection JSON blob. |
| 223 | */ |
| Akron | eedac91 | 2020-11-16 12:54:42 +0100 | [diff] [blame] | 224 | public JsonNode getCollection () { |
| 225 | return this.collection; |
| 226 | }; |
| 227 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 228 | |
| 229 | /** |
| 230 | * Set the maximum results to be fetched. |
| 231 | * |
| 232 | * This needs to be set prior to the first |
| 233 | * "addMatch" so it can be taken into account. |
| 234 | */ |
| 235 | public void setMaxResults (int maxResults) { |
| 236 | this.maxResults = maxResults; |
| 237 | }; |
| 238 | |
| 239 | |
| 240 | /** |
| 241 | * Get the maximum results to be fetched. |
| 242 | */ |
| 243 | public int getMaxResults () { |
| 244 | return this.maxResults; |
| 245 | }; |
| 246 | |
| 247 | |
| 248 | /** |
| 249 | * Get the export ID which is the pointer |
| 250 | * to where the system can find the temporary |
| 251 | * generated file. |
| 252 | */ |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 253 | public String getExportID () { |
| 254 | if (this.file == null) |
| 255 | return ""; |
| 256 | return this.file.getName(); |
| 257 | }; |
| 258 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 259 | |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 260 | /** |
| 261 | * Set the file based on the export ID |
| 262 | */ |
| 263 | public void setFile (String exportID) { |
| 264 | this.file = new File( |
| 265 | this.getFileDirectory(), |
| 266 | exportID |
| 267 | ); |
| 268 | } |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 269 | |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 270 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 271 | /** |
| 272 | * Write header for exportation. |
| 273 | * |
| 274 | * Should be overwritten. |
| 275 | */ |
| Akron | 1d36eb5 | 2020-11-13 17:52:26 +0100 | [diff] [blame] | 276 | public void writeHeader (Writer w) throws IOException { }; |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 277 | |
| 278 | |
| 279 | /** |
| 280 | * Write footer for exportation. |
| 281 | * |
| 282 | * Should be overwritten. |
| 283 | */ |
| Akron | 1d36eb5 | 2020-11-13 17:52:26 +0100 | [diff] [blame] | 284 | public void writeFooter (Writer w) throws IOException { }; |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 285 | |
| 286 | |
| 287 | /** |
| 288 | * Write a single match. |
| 289 | * |
| 290 | * Should be overwritten. |
| 291 | */ |
| Akron | 1d36eb5 | 2020-11-13 17:52:26 +0100 | [diff] [blame] | 292 | public void addMatch (JsonNode n, Writer w) throws IOException { }; |
| Akron | d0b1cfe | 2020-11-20 19:26:52 +0100 | [diff] [blame] | 293 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 294 | |
| 295 | /** |
| 296 | * Set the event stream for progress feedback. |
| 297 | */ |
| Akron | af145eb | 2020-11-24 16:55:47 +0100 | [diff] [blame] | 298 | public void setSse (EventOutput eventOutput) { |
| 299 | this.evOut = eventOutput; |
| Akron | d0b1cfe | 2020-11-20 19:26:52 +0100 | [diff] [blame] | 300 | }; |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 301 | |
| Akron | af145eb | 2020-11-24 16:55:47 +0100 | [diff] [blame] | 302 | |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 303 | /** |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 304 | * Force the creation of a file, even when only |
| 305 | * a few matches are requested. |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 306 | */ |
| 307 | public void forceFile () { |
| 308 | |
| 309 | // Open file if not already opened |
| 310 | if (this.file == null) { |
| 311 | |
| 312 | try { |
| 313 | |
| 314 | File dir = getFileDirectory(); |
| 315 | |
| 316 | // Create temporary file |
| 317 | this.file = File.createTempFile( |
| 318 | "idsexp-", "." + this.getSuffix(), |
| 319 | dir |
| 320 | ); |
| 321 | |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 322 | String s = null; |
| 323 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 324 | // Take temporary data from the in-memory writer |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 325 | if (writer != null) |
| 326 | s = writer.toString(); |
| 327 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 328 | // Establish persistant writer |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 329 | writer = new BufferedWriter(new FileWriter(this.file, true)); |
| 330 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 331 | // Add in-memory string |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 332 | if (s != null) |
| 333 | writer.write(s); |
| 334 | |
| 335 | } |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 336 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 337 | // If data can't be stored on disk, the writer will |
| 338 | // rely on in-memory data, which may or may not work in |
| 339 | // different contexts. |
| 340 | catch (IOException e) { |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 341 | return; |
| 342 | }; |
| 343 | }; |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 344 | }; |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 345 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 346 | |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 347 | /** |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 348 | * Parse initial JSON file to get header information |
| 349 | * and initial matches. |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 350 | */ |
| Akron | 62d90a3 | 2020-11-18 20:45:38 +0100 | [diff] [blame] | 351 | public boolean init (String resp) throws IOException, JsonParseException { |
| Akron | 1d36eb5 | 2020-11-13 17:52:26 +0100 | [diff] [blame] | 352 | |
| Akron | c51327b | 2020-11-13 15:21:26 +0100 | [diff] [blame] | 353 | if (resp == null) |
| Akron | 62d90a3 | 2020-11-18 20:45:38 +0100 | [diff] [blame] | 354 | return false; |
| 355 | |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 356 | JsonParser parser = mapper.getFactory().createParser(resp); |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 357 | JsonNode root = mapper.readTree(parser); |
| Akron | 62d90a3 | 2020-11-18 20:45:38 +0100 | [diff] [blame] | 358 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 359 | if (root == null) |
| Akron | 62d90a3 | 2020-11-18 20:45:38 +0100 | [diff] [blame] | 360 | return false; |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 361 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 362 | JsonNode meta = root.get("meta"); |
| Akron | c1c1824 | 2020-11-18 18:24:12 +0100 | [diff] [blame] | 363 | this.setMeta(meta); |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 364 | this.setQuery(root.get("query")); |
| 365 | this.setCollection(root.get("collection")); |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 366 | |
| Akron | c1c1824 | 2020-11-18 18:24:12 +0100 | [diff] [blame] | 367 | if (meta != null) { |
| 368 | if (meta.has("totalResults")) { |
| 369 | this.totalResults = meta.get("totalResults").asInt(); |
| 370 | if (meta.has("timeExceeded")) { |
| 371 | this.timeExceeded = meta.get("timeExceeded").asBoolean(); |
| 372 | }; |
| 373 | }; |
| 374 | }; |
| 375 | |
| Akron | ba3ea11 | 2020-11-24 22:40:18 +0100 | [diff] [blame] | 376 | // In case the writer is already set (e.g. forceFile() was issued), |
| 377 | // write in the header |
| 378 | if (writer == null) { |
| 379 | this.file = null; |
| 380 | writer = new StringWriter(); |
| 381 | }; |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 382 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 383 | // Write header to exporter |
| Akron | 1d36eb5 | 2020-11-13 17:52:26 +0100 | [diff] [blame] | 384 | this.writeHeader(writer); |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 385 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 386 | // Go on by iterating through matches |
| 387 | return this.iterateThroughMatches(root.get("matches")); |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 388 | }; |
| 389 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 390 | |
| 391 | /** |
| 392 | * Finalize the export stream. |
| 393 | */ |
| 394 | public Exporter finish() throws IOException { |
| 395 | this.writeFooter(this.writer); |
| 396 | this.writer.close(); |
| 397 | return (Exporter) this; |
| 398 | }; |
| Akron | c51327b | 2020-11-13 15:21:26 +0100 | [diff] [blame] | 399 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 400 | |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 401 | /** |
| 402 | * Append more matches to the result set. |
| 403 | */ |
| Akron | 62d90a3 | 2020-11-18 20:45:38 +0100 | [diff] [blame] | 404 | public boolean appendMatches (String resp) throws IOException { |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 405 | |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 406 | // Demand creation of a file |
| 407 | this.forceFile(); |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 408 | |
| 409 | JsonParser parser = mapper.getFactory().createParser(resp); |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 410 | JsonNode root = mapper.readTree(parser); |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 411 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 412 | if (root == null) |
| Akron | 62d90a3 | 2020-11-18 20:45:38 +0100 | [diff] [blame] | 413 | return false; |
| Akron | 1d36eb5 | 2020-11-13 17:52:26 +0100 | [diff] [blame] | 414 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 415 | return this.iterateThroughMatches(root.get("matches")); |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 416 | }; |
| 417 | |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 418 | |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 419 | /** |
| Akron | 1d36eb5 | 2020-11-13 17:52:26 +0100 | [diff] [blame] | 420 | * Serve response entity, either as a string or as a file. |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 421 | */ |
| Akron | 1d36eb5 | 2020-11-13 17:52:26 +0100 | [diff] [blame] | 422 | public ResponseBuilder serve () { |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 423 | |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 424 | ResponseBuilder rb; |
| Akron | e57937b | 2020-11-17 08:49:31 +0100 | [diff] [blame] | 425 | |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 426 | if (this.file == null) { |
| Akron | e57937b | 2020-11-17 08:49:31 +0100 | [diff] [blame] | 427 | |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 428 | // Serve stream |
| 429 | rb = Response.ok(writer.toString()); |
| Akron | 1d36eb5 | 2020-11-13 17:52:26 +0100 | [diff] [blame] | 430 | } |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 431 | else if (this.file.exists()) { |
| Akron | 1d36eb5 | 2020-11-13 17:52:26 +0100 | [diff] [blame] | 432 | |
| Akron | 55def63 | 2020-11-26 16:00:02 +0100 | [diff] [blame] | 433 | // Serve the file and delete after serving |
| 434 | final File expFile = this.file; |
| 435 | try { |
| 436 | final InputStream in = new FileInputStream(this.file); |
| 437 | |
| 438 | // Remove file after output is streamed |
| 439 | StreamingOutput output = new StreamingOutput() { |
| 440 | @Override |
| 441 | public void write(OutputStream out) |
| 442 | throws IOException { |
| 443 | |
| 444 | // Write file data in output stream |
| 445 | int length; |
| 446 | byte[] buffer = new byte[1024]; |
| 447 | while ((length = in.read(buffer)) != -1) { |
| 448 | out.write(buffer, 0, length); |
| 449 | } |
| 450 | out.flush(); // Important! |
| 451 | in.close(); |
| 452 | |
| 453 | // When done, delete the file |
| 454 | expFile.delete(); |
| 455 | } |
| 456 | }; |
| 457 | |
| 458 | // Serve file |
| 459 | rb = Response.ok(output); |
| 460 | } |
| 461 | |
| 462 | catch (Exception e) { |
| 463 | // File problematic |
| Akron | a3839bb | 2020-11-26 16:07:31 +0100 | [diff] [blame^] | 464 | return Response.status(Status.NOT_FOUND); |
| Akron | 55def63 | 2020-11-26 16:00:02 +0100 | [diff] [blame] | 465 | }; |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 466 | } |
| 467 | else { |
| 468 | // File doesn't exist |
| Akron | a3839bb | 2020-11-26 16:07:31 +0100 | [diff] [blame^] | 469 | return Response.status(Status.NOT_FOUND); |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 470 | }; |
| Akron | d0b1cfe | 2020-11-20 19:26:52 +0100 | [diff] [blame] | 471 | |
| Akron | 3588101 | 2020-11-24 20:05:06 +0100 | [diff] [blame] | 472 | return rb |
| 473 | .type(this.getMimeType()) |
| 474 | .header( |
| 475 | "Content-Disposition", |
| 476 | "attachment; filename=" + |
| 477 | this.getFileName() + |
| 478 | '.' + |
| 479 | this.getSuffix() |
| 480 | ); |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 481 | }; |
| Akron | 62d90a3 | 2020-11-18 20:45:38 +0100 | [diff] [blame] | 482 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 483 | |
| 484 | /* |
| 485 | * Iterate through all matches |
| 486 | */ |
| 487 | private boolean iterateThroughMatches (JsonNode mNodes) |
| 488 | throws IOException { |
| Akron | 62d90a3 | 2020-11-18 20:45:38 +0100 | [diff] [blame] | 489 | |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 490 | // Send progress information |
| Akron | d0b1cfe | 2020-11-20 19:26:52 +0100 | [diff] [blame] | 491 | this.sendProgress(); |
| 492 | |
| Akron | 62d90a3 | 2020-11-18 20:45:38 +0100 | [diff] [blame] | 493 | if (mNodes == null) |
| 494 | return false; |
| 495 | |
| 496 | // Iterate over the results of the current file |
| 497 | Iterator<JsonNode> mNode = mNodes.elements(); |
| 498 | while (mNode.hasNext()) { |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 499 | |
| 500 | // Stop if all relevant matches are fetched |
| Akron | 62d90a3 | 2020-11-18 20:45:38 +0100 | [diff] [blame] | 501 | if (this.maxResults > 0 && |
| Akron | 3524b8c | 2020-11-19 01:19:02 +0100 | [diff] [blame] | 502 | this.fetchedResults >= this.maxResults) { |
| Akron | 62d90a3 | 2020-11-18 20:45:38 +0100 | [diff] [blame] | 503 | return false; |
| 504 | }; |
| Akron | 3524b8c | 2020-11-19 01:19:02 +0100 | [diff] [blame] | 505 | this.addMatch(mNode.next(), writer); |
| 506 | this.fetchedResults++; |
| Akron | 62d90a3 | 2020-11-18 20:45:38 +0100 | [diff] [blame] | 507 | }; |
| 508 | return true; |
| 509 | }; |
| Akron | 984fe8f | 2020-11-25 15:21:37 +0100 | [diff] [blame] | 510 | |
| 511 | |
| 512 | /* |
| 513 | * Get the directory where all temporary files are stored. |
| 514 | */ |
| 515 | private File getFileDirectory () { |
| 516 | |
| 517 | String fileDir = prop.getProperty( |
| 518 | "conf.file_dir", |
| 519 | System.getProperty("java.io.tmpdir") |
| 520 | ); |
| 521 | |
| 522 | File dir = new File(fileDir); |
| 523 | |
| 524 | // Create directory if not yet existing |
| 525 | if (!dir.exists()) { |
| 526 | dir.mkdir(); |
| 527 | } |
| 528 | |
| 529 | // Directory is unwritable - fallback |
| 530 | else if (!dir.canWrite()) { |
| 531 | fileDir = System.getProperty("java.io.tmpdir"); |
| 532 | System.err.println("Unable to write to directory"); |
| 533 | System.err.println("Fallback to " + fileDir); |
| 534 | dir = new File(fileDir); |
| 535 | }; |
| 536 | return dir; |
| 537 | }; |
| 538 | |
| 539 | |
| 540 | /* |
| 541 | * Send a single progress event to the event stream. |
| 542 | */ |
| 543 | private void sendProgress () { |
| 544 | |
| 545 | if (this.evOut == null || this.maxResults == 0) |
| 546 | return; |
| 547 | |
| 548 | if (this.evOut.isClosed()) |
| 549 | return; |
| 550 | |
| 551 | int calc = (int) Math.ceil(((double) this.fetchedResults / this.maxResults) * 100); |
| 552 | |
| 553 | final OutboundEvent.Builder eventBuilder = new OutboundEvent.Builder(); |
| 554 | eventBuilder.name("Progress"); |
| 555 | eventBuilder.data(String.valueOf(calc)); |
| 556 | |
| 557 | try { |
| 558 | this.evOut.write(eventBuilder.build()); |
| 559 | } |
| 560 | catch (IOException e) { |
| 561 | return; |
| 562 | }; |
| 563 | }; |
| Akron | b329d27 | 2020-11-13 12:45:26 +0100 | [diff] [blame] | 564 | }; |