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