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