]> git.mxchange.org Git - jaddressbook-lib.git/blob - Addressbook/src/org/mxchange/addressbook/database/backend/csv/Base64CsvDatabaseBackend.java
Fixes for broken commit, as type-hints must be fixed or casted
[jaddressbook-lib.git] / Addressbook / src / org / mxchange / addressbook / database / backend / csv / 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.addressbook.database.backend.csv;
18
19 import java.io.DataOutput;
20 import java.io.FileNotFoundException;
21 import java.io.IOException;
22 import java.io.RandomAccessFile;
23 import java.sql.SQLException;
24 import java.text.MessageFormat;
25 import java.util.ArrayList;
26 import java.util.Base64;
27 import java.util.Iterator;
28 import java.util.List;
29 import java.util.StringTokenizer;
30 import org.mxchange.addressbook.FrameworkInterface;
31 import org.mxchange.addressbook.contact.Contact;
32 import org.mxchange.addressbook.contact.Gender;
33 import org.mxchange.addressbook.contact.book.BookContact;
34 import org.mxchange.addressbook.contact.user.UserContact;
35 import org.mxchange.addressbook.database.backend.BaseDatabaseBackend;
36 import org.mxchange.addressbook.database.backend.DatabaseBackend;
37 import org.mxchange.addressbook.database.storage.Storeable;
38 import org.mxchange.addressbook.database.storage.csv.StoreableCsv;
39 import org.mxchange.addressbook.exceptions.BadTokenException;
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 RandomAccessFile storageFile;
52
53         /**
54          * Constructor with table name
55          *
56          * @param tableName Name of "table"
57          */
58         public Base64CsvDatabaseBackend (final String tableName) {
59                 // Debug message
60                 this.getLogger().debug(MessageFormat.format("Trying to initialize table {0} ...", tableName)); //NOI18N
61
62                 // Set table name here, too
63                 this.setTableName(tableName);
64
65                 // Construct file name
66                 String fileName = String.format("data/table_%s.b64", tableName); //NOI18N
67
68                 // Debug message
69                 this.getLogger().debug(MessageFormat.format("Trying to open file {0} ...", fileName)); //NOI18N
70
71                 try {
72                         // Try to initialize the storage (file instance)
73                         this.storageFile = new RandomAccessFile(fileName, "rw"); //NOI18N
74                 } catch (final FileNotFoundException ex) {
75                         // Did not work
76                         this.getLogger().error(MessageFormat.format("File {0} cannot be opened: {1}", fileName, ex.toString())); //NOI18N
77                         System.exit(1);
78                 }
79
80                 // Output message
81                 this.getLogger().debug(MessageFormat.format("Database for {0} has been initialized.", tableName)); //NOI18N
82         }
83
84         /**
85          * This database backend does not need to connect
86          */
87         @Override
88         public void connectToDatabase () throws SQLException {
89                 // Empty body
90         }
91
92         /**
93          * Shuts down this backend
94          */
95         @Override
96         public void doShutdown () {
97                 // Trace message
98                 this.getLogger().trace("CALLED!"); //NOI18N
99
100                 try {
101                         // Close file
102                         this.getStorageFile().close();
103                 } catch (final IOException ex) {
104                         // Abort program
105                         this.abortProgramWithException(ex);
106                 }
107
108                 // Trace message
109                 this.getLogger().trace("EXIT!"); //NOI18N
110         }
111
112         /**
113          * Some "getter" for total row count
114          *
115          * @return Total row count
116          */
117         @Override
118         public int getTotalCount () {
119                 // Trace message
120                 this.getLogger().trace("CALLED!"); //NOI18N
121
122                 try {
123                         // Do a deprecated call
124                         // @todo this needs rewrite!
125                         return this.readList().size();
126                 } catch (final BadTokenException ex) {
127                         this.abortProgramWithException(ex);
128                 }
129
130                 // Invalid return
131                 this.getLogger().trace("Returning -1 ... : EXIT!"); //NOI18N
132                 return -1;
133         }
134
135         /**
136          * Checks whether at least one row is found with given boolean value.
137          *
138          * @param columnName Column to check for boolean value
139          * @param bool Boolean value to check
140          * @return Whether boolean value is found and returns at least one row
141          */
142         @Override
143         public boolean isRowFound (final String columnName, final boolean bool) {
144                 // Trace message
145                 this.getLogger().trace(MessageFormat.format("columnName={0},bool={1} - CALLED!", columnName, bool)); //NOI18N
146
147                 // Is at least one entry found?
148                 if (this.getTotalCount() == 0) {
149                         // No entry found at all
150                         return false;
151                 }
152
153                 // Default is not found
154                 boolean isFound = false;
155
156                 // Firsr rewind
157                 this.rewind();
158
159                 // Then loop through all lines
160                 while (!this.isEndOfFile()) {
161                         // Read line
162                         String line = this.readLine();
163
164                         // Debug message
165                         this.getLogger().debug(MessageFormat.format("line={0}", line));
166
167                         try {
168                                 // And parse it to a Contact instance
169                                 Contact contact = this.parseLineToContact(line);
170
171                                 // Debug message
172                                 this.getLogger().debug(MessageFormat.format("contact={0}", contact));
173
174                                 // This should not be null
175                                 if (contact == null) {
176                                         // Throw exception
177                                         throw new NullPointerException("contact is null");
178                                 }
179
180                                 // Now let the contact object check if it has such attribute
181                                 if (contact.isValueEqual(columnName, bool)) {
182                                         // Yes, it is set
183                                         isFound = true;
184                                         break;
185                                 }
186                         } catch (final BadTokenException ex) {
187                                 // Don't continue with bad data
188                                 this.abortProgramWithException(ex);
189                         }
190                 }
191
192                 // Trace message
193                 this.getLogger().trace(MessageFormat.format("isFound={0} - EXIT!", isFound));
194
195                 // Return result
196                 return isFound;
197         }
198
199         /**
200          * Gets an iterator for contacts
201          *
202          * @return Iterator for contacts
203          * @throws org.mxchange.addressbook.exceptions.BadTokenException If the underlaying method has found an invalid token
204          */
205         @Override
206         public Iterator<? extends Storeable> iterator () throws BadTokenException {
207                 // Trace message
208                 this.getLogger().trace("CALLED!"); //NOI18N
209                 
210                 /*
211                  * Then read the file into RAM (yes, not perfect for >1000 entries ...)
212                  * and get a List back.
213                  */
214                 List<? extends Storeable> list = this.readList();
215                 
216                 // List must be set
217                 assert (list instanceof List) : "list has not been set."; //NOI18N
218                 
219                 // Trace message
220                 this.getLogger().trace(MessageFormat.format("list.iterator()={0} - EXIT!", list.iterator())); //NOI18N
221                 
222                 // Get iterator from list and return it
223                 return list.iterator();
224         }
225
226         /**
227          * Get length of underlaying file
228          *
229          * @return Length of underlaying file
230          */
231         @Override
232         public long length () {
233                 long length = 0;
234
235                 try {
236                         length = this.getStorageFile().length();
237                         this.getLogger().debug(MessageFormat.format("length={0}", length)); //NOI18N
238                 } catch (final IOException ex) {
239                         // Length cannot be determined
240                         // Abort program
241                         this.abortProgramWithException(ex);
242                 }
243
244                 // Return result
245                 this.getLogger().trace(MessageFormat.format("length={0} : EXIT!", length)); //NOI18N
246                 return length;
247         }
248
249         /**
250          * Rewinds backend
251          */
252         @Override
253         public void rewind () {
254                 // Trace message
255                 this.getLogger().trace("CALLED!"); //NOI18N
256
257                 try {
258                         // Rewind underlaying database file
259                         this.getStorageFile().seek(0);
260                 } catch (final IOException ex) {
261                         // Abort program
262                         this.abortProgramWithException(ex);
263                 }
264
265                 // Trace message
266                 this.getLogger().trace("EXIT!"); //NOI18N
267         }
268
269         /**
270          * Stores given object by "visiting" it
271          *
272          * @param object An object implementing Storeable
273          * @throws java.io.IOException From "inner" class
274          */
275         @Override
276         public void store (final Storeable object) throws IOException {
277                 // Trace message
278                 this.getLogger().trace(MessageFormat.format("object={0} - CALLED!", object)); //NOI18N
279
280                 // Object must not be null
281                 if (object == null) {
282                         // Abort here
283                         throw new NullPointerException("object is null"); //NOI18N
284                 }
285
286                 // Make sure the instance is there (DataOutput flawor)
287                 assert (this.storageFile instanceof DataOutput);
288
289                 // Try to cast it, this will fail if the interface is not implemented
290                 StoreableCsv csv = (StoreableCsv) object;
291
292                 // Now get a string from the object that needs to be stored
293                 String str = csv.getCsvStringFromStoreableObject();
294
295                 // Debug message
296                 this.getLogger().debug(MessageFormat.format("str({0})={1}", str.length(), str)); //NOI18N
297
298                 // Encode line in BASE-64
299                 byte[] encoded = Base64.getEncoder().encode(str.getBytes());
300
301                 // The string is now a valid CSV string
302                 this.getStorageFile().write(encoded);
303
304                 // Trace message
305                 this.getLogger().trace("EXIT!"); //NOI18N
306         }
307
308         /**
309          * Adds given contact to list
310          *
311          * @param instance An instance of FrameworkInterface to add
312          * @param list List instance
313          */
314         private void addToList (final Storeable instance, final List<Storeable> list) {
315                 // Trace message
316                 this.getLogger().trace(MessageFormat.format("contact={0} - CALLED!", instance)); //NOI18N
317
318                 // No null here
319                 if (instance == null) {
320                         // Throw exception
321                         throw new NullPointerException("contact is null"); //NOI18N
322                 } else if (list == null) {
323                         // Throw exception
324                         throw new NullPointerException("list is null"); //NOI18N
325                 }
326
327                 // Debug message
328                 this.getLogger().debug(MessageFormat.format("contact={0}", instance)); //NOI18N
329
330                 // Is the contact read?
331                 if (instance instanceof FrameworkInterface) {
332                         // Then add it
333                         boolean added = list.add(instance);
334
335                         // Debug message
336                         this.getLogger().debug(MessageFormat.format("contact={0} added={1}", instance, added)); //NOI18N
337
338                         // Has it been added?
339                         if (!added) {
340                                 // Not added
341                                 this.getLogger().warn("Contact object has not been added."); //NOI18N
342                         }
343                 }
344
345                 // Trace message
346                 this.getLogger().trace("EXIT!"); //NOI18N
347         }
348
349         /**
350          * Returns storage file
351          *
352          * @return Storage file instance
353          */
354         private RandomAccessFile getStorageFile () {
355                 return this.storageFile;
356         }
357
358         /**
359          * Checks whether end of file has been reached
360          *
361          * @return Whether lines are left to read
362          */
363         private boolean isEndOfFile () {
364                 // Default is EOF
365                 boolean isEof = true;
366
367                 try {
368                         isEof = (this.getStorageFile().getFilePointer() >= this.length());
369                 } catch (final IOException ex) {
370                         // Length cannot be determined
371                         this.getLogger().catching(ex);
372                 }
373
374                 // Return status
375                 this.getLogger().trace(MessageFormat.format("isEof={0} : EXIT!", isEof)); //NOI18N
376                 return isEof;
377         }
378
379         /**
380          * Parses given line and creates a Contact instance
381          * 
382          * @param line Raw line to parse
383          *
384          * @return Contact instance
385          */
386         private Contact parseLineToContact (final String line) throws BadTokenException {
387                 // Trace message
388                 this.getLogger().trace(MessageFormat.format("line={0} - CALLED!", line)); //NOI18N
389
390                 // Init A lot variables
391                 Long num = null;
392                 Boolean bool = null;
393                 Gender gender = null;
394                 int count = 0;
395                 Contact contact = null;
396
397                 // Debug message
398                 this.getLogger().debug(MessageFormat.format("line={0}", line)); //NOI18N
399
400                 // Then tokenize it
401                 // @TODO Move this into separate method
402                 StringTokenizer tokenizer = new StringTokenizer(line, ";"); //NOI18N
403
404                 // Reset variables
405                 count = 0;
406                 contact = null;
407
408                 // The tokens are now available, so get all
409                 while (tokenizer.hasMoreElements()) {
410                         // Reset variables
411                         num = null;
412                         bool = null;
413
414                         // Get next token
415                         String token = tokenizer.nextToken();
416
417                         // If char " is at pos 2 (0,1,2), then cut it of there
418                         if ((token.charAt(0) != '"') && (token.charAt(2) == '"')) {
419                                 // UTF-8 writer characters found
420                                 token = token.substring(2);
421                         }
422
423                         // Debug message
424                         this.getLogger().debug(MessageFormat.format("token={0}", token)); //NOI18N
425
426                         // Verify token, it must have double-quotes on each side
427                         if ((!token.startsWith("\"")) || (!token.endsWith("\""))) { //NOI18N
428                                 // Something bad was read
429                                 throw new BadTokenException(token, count); //NOI18N
430                         }
431
432                         // All fine, so remove it
433                         String strippedToken = token.substring(1, token.length() - 1);
434
435                         // Is the string's content "null"?
436                         if (strippedToken.equals("null")) { //NOI18N
437                                 // Debug message
438                                 this.getLogger().debug(MessageFormat.format("strippedToken={0} - NULL!", strippedToken)); //NOI18N
439
440                                 // This needs to be set to null
441                                 strippedToken = null;
442                         }
443
444                         // Debug message
445                         this.getLogger().debug(MessageFormat.format("strippedToken={0}", strippedToken)); //NOI18N
446
447                         // Now, let's try a number check, if no null
448                         if (strippedToken != null) {
449                                 // Okay, no null, maybe the string bears a decimal number?
450                                 try {
451                                         num = Long.valueOf(strippedToken);
452
453                                         // Debug message
454                                         this.getLogger().debug(MessageFormat.format("strippedToken={0} - NUMBER!", strippedToken)); //NOI18N
455                                 } catch (final NumberFormatException ex) {
456                                         // No number, then set default
457                                         num = null;
458                                 }
459                         }
460
461                         // Now, let's try a boolean check, if no null
462                         if ((strippedToken != null) && (num == null) && ((strippedToken.equals("true")) || (strippedToken.equals("false")))) { //NOI18N
463                                 // Debug message
464                                 this.getLogger().debug(MessageFormat.format("strippedToken={0} - BOOLEAN!", strippedToken)); //NOI18N
465
466                                 // parseBoolean() is relaxed, so no exceptions
467                                 bool = Boolean.valueOf(strippedToken);
468                         }
469
470                         // Debug message
471                         this.getLogger().debug(MessageFormat.format("strippedToken={0},num={1},bool={2}", strippedToken, num, bool)); //NOI18N
472
473                         // Now, let's try a gender check, if no null
474                         if ((strippedToken != null) && (num == null) && (bool == null) && ((strippedToken.equals("M")) || (strippedToken.equals("F")) || (strippedToken.equals("C")))) { //NOI18N
475                                 // Get first character
476                                 gender = Gender.fromChar(strippedToken.charAt(0));
477
478                                 // Debug message
479                                 this.getLogger().debug(MessageFormat.format("strippedToken={0},gender={1}", strippedToken, gender)); //NOI18N
480
481                                 // This instance must be there
482                                 assert (gender instanceof Gender) : MessageFormat.format("gender is not set by Gender.fromChar({0})", strippedToken); //NOI18N
483                         }
484
485                         // Now it depends on the counter which position we need to check
486                         switch (count) {
487                                 case 0: // isOwnContact
488                                         assert ((bool instanceof Boolean));
489
490                                         // Debug message
491                                         this.getLogger().debug(MessageFormat.format("bool={0}", bool)); //NOI18N
492
493                                         // Is it own contact?
494                                         if (true == bool) {
495                                                 // Debug message
496                                                 this.getLogger().debug("Creating UserContact object ..."); //NOI18N
497
498                                                 // Own entry
499                                                 contact = new UserContact();
500                                         } else {
501                                                 // Debug message
502                                                 this.getLogger().debug("Creating BookContact object ..."); //NOI18N
503
504                                                 // Other contact
505                                                 contact = new BookContact();
506                                         }
507                                         break;
508
509                                 case 1: // Gender
510                                         assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
511
512                                         // Update data
513                                         contact.updateNameData(gender, null, null, null);
514                                         break;
515
516                                 case 2: // Surname
517                                         assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
518                                         assert (gender instanceof Gender) : "gender instance is not set"; //NOI18N
519
520                                         // Update data
521                                         contact.updateNameData(gender, strippedToken, null, null);
522                                         break;
523
524                                 case 3: // Family name
525                                         assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
526                                         assert (gender instanceof Gender) : "gender instance is not set"; //NOI18N
527
528                                         // Update data
529                                         contact.updateNameData(gender, null, strippedToken, null);
530                                         break;
531
532                                 case 4: // Company name
533                                         assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
534                                         assert (gender instanceof Gender) : "gender instance is not set"; //NOI18N
535
536                                         // Update data
537                                         contact.updateNameData(gender, null, null, strippedToken);
538                                         break;
539
540                                 case 5: // Street number
541                                         assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
542
543                                         // Update data
544                                         contact.updateAddressData(strippedToken, 0, null, null);
545                                         break;
546
547                                 case 6: // ZIP code
548                                         assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
549
550                                         // Update data
551                                         contact.updateAddressData(null, num, null, null);
552                                         break;
553
554                                 case 7: // City name
555                                         assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
556
557                                         // Update data
558                                         contact.updateAddressData(null, 0, strippedToken, null);
559                                         break;
560
561                                 case 8: // Country code
562                                         assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
563
564                                         // Update data
565                                         contact.updateAddressData(null, 0, null, strippedToken);
566                                         break;
567
568                                 case 9: // Phone number
569                                         assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
570
571                                         // Update data
572                                         contact.updateOtherData(strippedToken, null, null, null, null, null);
573                                         break;
574
575                                 case 10: // Fax number
576                                         assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
577
578                                         // Update data
579                                         contact.updateOtherData(null, strippedToken, null, null, null, null);
580                                         break;
581
582                                 case 11: // Cellphone number
583                                         assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
584
585                                         // Update data
586                                         contact.updateOtherData(null, null, strippedToken, null, null, null);
587                                         break;
588
589                                 case 12: // Email address
590                                         assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
591
592                                         // Update data
593                                         contact.updateOtherData(null, null, null, strippedToken, null, null);
594                                         break;
595
596                                 case 13: // Birthday
597                                         assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
598
599                                         // Update data
600                                         contact.updateOtherData(null, null, null, null, strippedToken, null);
601                                         break;
602
603                                 case 14: // Birthday
604                                         assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
605
606                                         // Update data
607                                         contact.updateOtherData(null, null, null, null, null, strippedToken);
608                                         break;
609
610                                 default: // New data entry
611                                         this.getLogger().warn(MessageFormat.format("Will not handle unknown data {0} at index {1}", strippedToken, count)); //NOI18N
612                                         break;
613                         }
614
615                         // Increment counter for next round
616                         count++;
617                 }
618
619                 // Trace message
620                 this.getLogger().trace(MessageFormat.format("contact={0} - EXIT!", contact)); //NOI18N
621
622                 // Return finished instance
623                 return contact;
624         }
625
626         /**
627          * Reads a line from file base
628          *
629          * @return Read line from file
630          */
631         private String readLine () {
632                 // Trace message
633                 this.getLogger().trace("CALLED!"); //NOI18N
634                 
635                 // Init input
636                 String input = null;
637                 
638                 try {
639                         // Read single line
640                         String base64 = this.getStorageFile().readLine();
641                         
642                         // Decode BASE-64
643                         byte[] decoded = Base64.getDecoder().decode(base64);
644                         
645                         // Convert to string
646                         input = new String(decoded);
647                 } catch (final IOException ex) {
648                         this.getLogger().catching(ex);
649                 }
650                 
651                 // Trace message
652                 this.getLogger().trace(MessageFormat.format("input={0} - EXIT!", input)); //NOI18N
653                 
654                 // Return read string or null
655                 return input;
656         }
657
658         /**
659          * Reads the database file, if available, and adds all read lines into the
660          * list.
661          *
662          * @return A list with Contact instances
663          */
664         private List<? extends Storeable> readList () throws BadTokenException {
665                 this.getLogger().trace("CALLED!"); //NOI18N
666
667                 // First rewind
668                 this.rewind();
669
670                 // Get file size and divide it by 140 (possible average length of one line)
671                 int lines = Math.round(this.length() / 140 + 0.5f);
672
673                 // Debug message
674                 this.getLogger().debug(MessageFormat.format("lines={0}", lines)); //NOI18N
675
676                 // Instance list
677                 // @TODO The maximum length could be guessed from file size?
678                 List<Storeable> list = new ArrayList<>(lines);
679
680                 // Init variables
681                 String line;
682                 Storeable instance = null;
683
684                 // Read all lines
685                 while (!this.isEndOfFile()) {
686                         // Then read a line
687                         line = this.readLine();
688
689                         // Parse line
690                         instance = (Storeable) this.parseLineToContact(line);
691
692                         // The contact instance should be there now
693                         assert (instance instanceof FrameworkInterface) : MessageFormat.format("instance is not set: {0}", instance); //NOI18N
694
695                         // Add contact
696                         this.addToList(instance, list);
697                 }
698
699                 // Return finished list
700                 this.getLogger().trace(MessageFormat.format("list.size()={0} : EXIT!", list.size())); //NOI18N
701                 return list;
702         }
703 }