]> git.mxchange.org Git - jcore.git/blob - src/org/mxchange/jcore/database/backend/base64/Base64CsvDatabaseBackend.java
Fixed lib path
[jcore.git] / src / org / mxchange / jcore / database / backend / base64 / Base64CsvDatabaseBackend.java
1 /*
2  * Copyright (C) 2015 Roland Haeder
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17 package org.mxchange.jcore.database.backend.base64;
18
19 import org.mxchange.jcore.database.backend.file.DatabaseFile;
20 import java.io.FileNotFoundException;
21 import java.io.IOException;
22 import java.io.RandomAccessFile;
23 import java.lang.reflect.InvocationTargetException;
24 import java.text.MessageFormat;
25 import java.util.HashMap;
26 import java.util.Iterator;
27 import java.util.Map;
28 import java.util.regex.Matcher;
29 import java.util.regex.Pattern;
30 import org.apache.commons.codec.binary.Base64;
31 import org.mxchange.jcore.criteria.searchable.SearchableCriteria;
32 import org.mxchange.jcore.database.backend.BaseDatabaseBackend;
33 import org.mxchange.jcore.database.backend.DatabaseBackend;
34 import org.mxchange.jcore.database.backend.file.SynchronizeableFile;
35 import org.mxchange.jcore.database.frontend.DatabaseFrontend;
36 import org.mxchange.jcore.database.result.DatabaseResult;
37 import org.mxchange.jcore.database.result.Result;
38 import org.mxchange.jcore.database.storage.Storeable;
39 import org.mxchange.jcore.exceptions.BadTokenException;
40 import org.mxchange.jcore.exceptions.CorruptedDatabaseFileException;
41
42 /**
43  * A database backend with CSV file as storage implementation
44  *
45  * @author Roland Haeder
46  */
47 public class Base64CsvDatabaseBackend extends BaseDatabaseBackend implements DatabaseBackend {
48
49         /**
50          * Output stream for this storage engine
51          */
52         private final SynchronizeableFile storageFile;
53
54         /**
55          * File name to access
56          */
57         private final String fileName;
58
59         /**
60          * Constructor with table name
61          *
62          * @param frontend Wrapper instance to call back
63          * @throws java.io.FileNotFoundException If the file was not found
64          */
65         public Base64CsvDatabaseBackend (final DatabaseFrontend frontend) throws FileNotFoundException {
66                 // Trace message
67                 this.getLogger().trace(MessageFormat.format("frontend={0} - CALLED!", frontend)); //NOI18N
68
69                 // Get table name
70                 String tableName = frontend.getTableName();
71
72                 // Debug message
73                 this.getLogger().debug(MessageFormat.format("Trying to initialize table {0} ...", tableName)); //NOI18N
74
75                 // Set table name here, too
76                 this.setTableName(tableName);
77
78                 // Set frontend here
79                 this.setFrontend(frontend);
80
81                 // Construct file name
82                 this.fileName = String.format("%s/table_%s.b64", this.getProperty("database.backend.storagepath"), tableName); //NOI18N
83
84                 // Debug message
85                 this.getLogger().debug(MessageFormat.format("Trying to open file {0} ...", this.fileName)); //NOI18N
86
87                 try {
88                         // Try to initialize the storage (file instance)
89                         this.storageFile = new DatabaseFile(this.fileName); //NOI18N
90                 } catch (final FileNotFoundException ex) {
91                         // Did not work
92                         this.getLogger().error(MessageFormat.format("File {0} cannot be opened: {1}", this.fileName, ex.toString())); //NOI18N
93                         throw ex;
94                 }
95
96                 // Output message
97                 this.getLogger().debug(MessageFormat.format("Database for {0} has been initialized.", tableName)); //NOI18N
98         }
99
100         /**
101          * This database backend does not need to connect
102          */
103         @Override
104         public void connectToDatabase () {
105                 // Empty body
106         }
107
108         @Override
109         public Result<? extends Storeable> doInsertDataSet (final Map<String, Object> dataset) throws IOException {
110                 // Trace message
111                 this.getLogger().trace(MessageFormat.format("dataset={0} - CALLED!", dataset));
112
113                 // dataset should not be null and not empty
114                 if (dataset == null) {
115                         // It is null, so abort here
116                         throw new NullPointerException("dataset is null");
117                 } else if (dataset.isEmpty()) {
118                         // It is empty, also abort here
119                         throw new IllegalArgumentException("dataset is empty");
120                 }
121
122                 // Debug message
123                 this.getLogger().debug(MessageFormat.format("Need to parse {0} values ...", dataset.size()));
124
125                 // Get iterator from it
126                 Iterator<Map.Entry<String, Object>> iterator = dataset.entrySet().iterator();
127
128                 // Full output string
129                 StringBuilder output = new StringBuilder(dataset.size() * 20);
130
131                 // Add index column
132                 output.append(String.format("key=%s,value=\"%s\";", this.getFrontend().getIdName(), this.getTotalRows() + 1));
133
134                 // "Walk" over all entries
135                 while (iterator.hasNext()) {
136                         // Get next entry
137                         Map.Entry<String, Object> entry = iterator.next();
138
139                         // Get value
140                         Object value = entry.getValue();
141
142                         // Validate value, should not contain "
143                         if (value instanceof String) {
144                                 // Is String so cast ist
145                                 String str = (String) value;
146
147                                 // Does it contain a " ?
148                                 if (str.contains("\"")) {
149                                         // Don't accept here
150                                         throw new IllegalArgumentException("value " + value + " with double-quote not supported yet.");
151                                 }
152                         }
153
154                                         // Generate key=value pair
155                         String pair = String.format("key=%s,value=\"%s\";", entry.getKey(), String.valueOf(value));
156
157                         // Debug message
158                         this.getLogger().debug("pair=" + pair);
159
160                         // Append to output
161                         output.append(pair);
162                 }
163
164                 // Then write it to file
165                 this.writeData(output);
166
167                 // The result set needs to be transformed into Result, so initialize a result instance here
168                 Result<? extends Storeable> result = new DatabaseResult();
169
170                 // Trace message
171                 this.getLogger().trace(MessageFormat.format("result={0} - EXIT!", result));
172
173                 // Return it
174                 return result;
175         }
176
177         @Override
178         public Result<? extends Storeable> doSelectByCriteria (final SearchableCriteria critera) throws IOException, BadTokenException, CorruptedDatabaseFileException, NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
179                 // Trace message
180                 this.getLogger().trace(MessageFormat.format("criteria={0} - CALLED!", critera));
181
182                 // Init result instance
183                 Result<Storeable> result = new DatabaseResult();
184
185                 // First rewind this backend
186                 this.rewind();
187
188                 // Then loop over all rows until the end has reached
189                 while (!this.isEndOfFile()) {
190                         // Read line
191                         String line = this.readLine();
192
193                         // Debug message
194                         this.getLogger().debug(MessageFormat.format("line={0}", line));
195
196                         // Parse it to a Map<String, Object>
197                         Map<String, String> map = this.getMapFromLine(line);
198
199                         // Convert it to a Storeable instance
200                         Storeable storeable = this.getFrontend().toStoreable(map);
201
202                         // Debug message
203                         this.getLogger().debug(MessageFormat.format("storeable={0}", storeable));
204
205                         // Now matches the found instance
206                         if (critera.matches(storeable)) {
207                                 // Then add it to result
208                                 result.add(storeable);
209                         }
210                 }
211
212                 // Return the result
213                 return result;
214         }
215
216         /**
217          * Shuts down this backend
218          */
219         @Override
220         public void doShutdown () throws IOException {
221                 // Trace message
222                 this.getLogger().trace("CALLED!"); //NOI18N
223
224                 // Close file
225                 this.getStorageFile().close();
226
227                 // Trace message
228                 this.getLogger().trace("EXIT!"); //NOI18N
229         }
230
231         /**
232          * Returns storage file
233          *
234          * @return Storage file instance
235          */
236         private SynchronizeableFile getStorageFile () {
237                 return this.storageFile;
238         }
239
240         /**
241          * Checks whether end of file has been reached
242          *
243          * @return Whether lines are left to read
244          */
245         private boolean isEndOfFile () {
246                 // Default is EOF
247                 boolean isEof = true;
248
249                 try {
250                         isEof = (this.getStorageFile().getFilePointer() >= this.length());
251                 } catch (final IOException ex) {
252                         // Length cannot be determined
253                         this.getLogger().catching(ex);
254                 }
255
256                 // Return status
257                 this.getLogger().trace(MessageFormat.format("isEof={0} : EXIT!", isEof)); //NOI18N
258                 return isEof;
259         }
260
261         /**
262          * Get length of underlaying file
263          *
264          * @return Length of underlaying file
265          */
266         private long length () throws IOException {
267                 long length = 0;
268
269                 // Try to get length from file
270                 length = this.getStorageFile().length();
271                 this.getLogger().debug(MessageFormat.format("length={0}", length)); //NOI18N
272
273                 // Return result
274                 this.getLogger().trace(MessageFormat.format("length={0} : EXIT!", length)); //NOI18N
275                 return length;
276         }
277
278         /**
279          * Reads a line from file base
280          *
281          * @return Read line from file
282          */
283         private String readLine () {
284                 // Trace message
285                 this.getLogger().trace("CALLED!"); //NOI18N
286
287                 // Init input
288                 String input = null;
289
290                 try {
291                         // Read single line
292                         String base64 = this.getStorageFile().readLine();
293
294                         // Is the line null?
295                         if (base64 == null) {
296                                 // Then throw NPE here
297                                 throw new NullPointerException("base64 is null, maybe missed to call isEndOfFile() ?"); //NOI18N
298                         }
299
300                         // Decode BASE-64
301                         byte[] decoded = Base64.decodeBase64(base64);
302
303                         // Convert to string
304                         input = new String(decoded).trim();
305                 } catch (final IOException ex) {
306                         this.getLogger().catching(ex);
307                 }
308
309                 // Trace message
310                 this.getLogger().trace(MessageFormat.format("input={0} - EXIT!", input)); //NOI18N
311
312                 // Return read string or null
313                 return input;
314         }
315
316         /**
317          * Rewinds backend
318          */
319         private void rewind () throws IOException {
320                 // Trace message
321                 this.getLogger().trace("CALLED!"); //NOI18N
322
323                 // Rewind underlaying database file
324                 this.getStorageFile().seek(0);
325
326                 // Trace message
327                 this.getLogger().trace("EXIT!"); //NOI18N
328         }
329
330         /**
331          * Writes a line with BASE64 encoding to database file
332          *
333          * @param output Output string to write
334          */
335         private void writeData (final StringBuilder output) throws IOException {
336                 // Trace message
337                 this.getLogger().trace("output=" + output + " - CALLED!");
338
339                 // No null or empty strings
340                 if (output == null) {
341                         // Is null
342                         throw new NullPointerException("output is null");
343                 } else  if (output.length() == 0) {
344                         // Is empty
345                         throw new IllegalArgumentException("output is empty");
346                 }
347
348                 // Encode it to BASE64
349                 String rawOutput = Base64.encodeBase64String(output.toString().getBytes());
350
351                 // Debug message
352                 this.getLogger().debug("rawOutput=" + rawOutput);
353
354                 // Write each line separately
355                 this.getStorageFile().writeBytes(rawOutput + "\n");
356
357                 // Trace message
358                 this.getLogger().trace("EXIT!");
359         }
360
361         /**
362          * Tries to interpret the given decoded line and puts its key/value pairs into a map.
363          *
364          * @param line Decoded line from database file
365          * @return A Map with keys and values from line
366          * @throws org.mxchange.jcore.exceptions.CorruptedDatabaseFileException If the file is believed damaged
367          * @throws org.mxchange.jcore.exceptions.BadTokenException If a bad token was found
368          */
369         private Map<String, String> getMapFromLine (final String line) throws CorruptedDatabaseFileException, BadTokenException {
370                 // Trace message
371                 this.getLogger().debug("line=" + line + " - CALLED!");
372
373                 // "line" must not be null or empty
374                 if (line == null) {
375                         // Is null
376                         throw new NullPointerException("line is null");
377                 } else if (line.isEmpty()) {
378                         // Is empty
379                         throw new IllegalArgumentException("line is empty, maybe isEndOfFile() was not called?");
380                 } else if (!line.endsWith(";")) {
381                         // Bad line found
382                         throw new CorruptedDatabaseFileException(this.fileName, "No semicolon at end of line");
383                 } else if (!line.contains("key=")) {
384                         // Bad line found
385                         throw new CorruptedDatabaseFileException(this.fileName, "No \"key=bla\" found.");
386                 } else if (!line.contains("value=")) {
387                         // Bad line found
388                         throw new CorruptedDatabaseFileException(this.fileName, "No \"value=bla\" found.");
389                 }
390
391                 Pattern pattern = Pattern.compile("(key=([a-z0-9_]{1,}),value=\"([^\"]*)\";){1,}");
392                 Matcher matcher = pattern.matcher(line);
393
394                 // Debug message
395                 this.getLogger().debug("matches=" + matcher.matches());
396
397                 // Matches?
398                 if (!matcher.matches()) {
399                         // Corrupted file found
400                         throw new CorruptedDatabaseFileException(this.fileName, "line " + line + " doesn't match regular expression.");
401                 }
402
403                 // Instance map
404                 Map<String, String> map = new HashMap<>(line.length() / 40);
405
406                 pattern = Pattern.compile("(key=([a-z0-9_]{1,}),value=\"([^\"]*)\";)");
407                 matcher = pattern.matcher(line);
408
409                 // Init group count
410                 int init = 0;
411
412                 // Then get all
413                 while (matcher.find(init)) {
414                         // Get group match
415                         String match = matcher.group(1);
416                         String key = matcher.group(2);
417                         String value = matcher.group(3);
418
419                         // key must noch be empty
420                         assert((key != null) && (!key.isEmpty())) : "key=" + key + " is not valid";
421
422                         // Get start and end
423                         int start = matcher.start();
424                         int end = matcher.end();
425
426                         // Debug message
427                         this.getLogger().debug("init=" + init + ",start=" + start + ",end=" + end + ",match=" + match + ",key=" + key + ",value=" + value);
428
429                         // Add key/value to map
430                         map.put(key, value);
431
432                         // Hop to next match
433                         init = end;
434                 }
435
436                 // Trace message
437                 this.getLogger().trace("map()=" + map.size() + " - EXIT!");
438
439                 // Return finished map
440                 return map;
441         }
442
443         @Override
444         public final Long getTotalRows () throws IOException {
445                 // Trace message
446                 this.getLogger().trace("CALLED!");
447
448                 // Init count
449                 Long count = 0L;
450
451                 // First rewind
452                 this.rewind();
453
454                 // Walk through all rows
455                 while (!this.isEndOfFile()) {
456                         // Get next line
457                         String line = this.readLine();
458
459                         // Count one up
460                         count++;
461                 }
462
463                 // Trace message
464                 this.getLogger().trace("count=" + count + " - EXIT!");
465
466                 // Return it
467                 return count;
468         }
469 }