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