2 * Copyright (C) 2015 Roland Haeder
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.
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.
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/>.
17 package org.mxchange.addressbook.database.backend.csv;
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;
42 * A database backend with CSV file as storage implementation
44 * @author Roland Haeder
46 public class Base64CsvDatabaseBackend extends BaseDatabaseBackend implements DatabaseBackend {
49 * Output stream for this storage engine
51 private RandomAccessFile storageFile;
54 * Constructor with table name
56 * @param tableName Name of "table"
58 public Base64CsvDatabaseBackend (final String tableName) {
60 this.getLogger().debug(MessageFormat.format("Trying to initialize table {0} ...", tableName)); //NOI18N
62 // Set table name here, too
63 this.setTableName(tableName);
65 // Construct file name
66 String fileName = String.format("data/table_%s.b64", tableName); //NOI18N
69 this.getLogger().debug(MessageFormat.format("Trying to open file {0} ...", fileName)); //NOI18N
72 // Try to initialize the storage (file instance)
73 this.storageFile = new RandomAccessFile(fileName, "rw"); //NOI18N
74 } catch (final FileNotFoundException ex) {
76 this.getLogger().error(MessageFormat.format("File {0} cannot be opened: {1}", fileName, ex.toString())); //NOI18N
81 this.getLogger().debug(MessageFormat.format("Database for {0} has been initialized.", tableName)); //NOI18N
85 * This database backend does not need to connect
88 public void connectToDatabase () throws SQLException {
93 * Shuts down this backend
96 public void doShutdown () {
98 this.getLogger().trace("CALLED!"); //NOI18N
102 this.getStorageFile().close();
103 } catch (final IOException ex) {
105 this.abortProgramWithException(ex);
109 this.getLogger().trace("EXIT!"); //NOI18N
113 * Some "getter" for total row count
115 * @return Total row count
118 public int getTotalCount () {
120 this.getLogger().trace("CALLED!"); //NOI18N
123 // Do a deprecated call
124 // @todo this needs rewrite!
125 return this.readList().size();
126 } catch (final BadTokenException ex) {
127 this.abortProgramWithException(ex);
131 this.getLogger().trace("Returning -1 ... : EXIT!"); //NOI18N
136 * Checks whether at least one row is found with given boolean value.
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
143 public boolean isRowFound (final String columnName, final boolean bool) {
145 this.getLogger().trace(MessageFormat.format("columnName={0},bool={1} - CALLED!", columnName, bool)); //NOI18N
147 // Is at least one entry found?
148 if (this.getTotalCount() == 0) {
149 // No entry found at all
153 // Default is not found
154 boolean isFound = false;
159 // Then loop through all lines
160 while (!this.isEndOfFile()) {
162 String line = this.readLine();
165 this.getLogger().debug(MessageFormat.format("line={0}", line));
168 // And parse it to a Contact instance
169 Contact contact = this.parseLineToContact(line);
172 this.getLogger().debug(MessageFormat.format("contact={0}", contact));
174 // This should not be null
175 if (contact == null) {
177 throw new NullPointerException("contact is null");
180 // Now let the contact object check if it has such attribute
181 if (contact.isValueEqual(columnName, bool)) {
186 } catch (final BadTokenException ex) {
187 // Don't continue with bad data
188 this.abortProgramWithException(ex);
193 this.getLogger().trace(MessageFormat.format("isFound={0} - EXIT!", isFound));
200 * Gets an iterator for contacts
202 * @return Iterator for contacts
203 * @throws org.mxchange.addressbook.exceptions.BadTokenException If the underlaying method has found an invalid token
206 public Iterator<? extends Storeable> iterator () throws BadTokenException {
208 this.getLogger().trace("CALLED!"); //NOI18N
211 * Then read the file into RAM (yes, not perfect for >1000 entries ...)
212 * and get a List back.
214 List<? extends Storeable> list = this.readList();
217 assert (list instanceof List) : "list has not been set."; //NOI18N
220 this.getLogger().trace(MessageFormat.format("list.iterator()={0} - EXIT!", list.iterator())); //NOI18N
222 // Get iterator from list and return it
223 return list.iterator();
227 * Get length of underlaying file
229 * @return Length of underlaying file
232 public long length () {
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
241 this.abortProgramWithException(ex);
245 this.getLogger().trace(MessageFormat.format("length={0} : EXIT!", length)); //NOI18N
253 public void rewind () {
255 this.getLogger().trace("CALLED!"); //NOI18N
258 // Rewind underlaying database file
259 this.getStorageFile().seek(0);
260 } catch (final IOException ex) {
262 this.abortProgramWithException(ex);
266 this.getLogger().trace("EXIT!"); //NOI18N
270 * Stores given object by "visiting" it
272 * @param object An object implementing Storeable
273 * @throws java.io.IOException From "inner" class
276 public void store (final Storeable object) throws IOException {
278 this.getLogger().trace(MessageFormat.format("object={0} - CALLED!", object)); //NOI18N
280 // Object must not be null
281 if (object == null) {
283 throw new NullPointerException("object is null"); //NOI18N
286 // Make sure the instance is there (DataOutput flawor)
287 assert (this.storageFile instanceof DataOutput);
289 // Try to cast it, this will fail if the interface is not implemented
290 StoreableCsv csv = (StoreableCsv) object;
292 // Now get a string from the object that needs to be stored
293 String str = csv.getCsvStringFromStoreableObject();
296 this.getLogger().debug(MessageFormat.format("str({0})={1}", str.length(), str)); //NOI18N
298 // Encode line in BASE-64
299 byte[] encoded = Base64.getEncoder().encode(str.getBytes());
301 // The string is now a valid CSV string
302 this.getStorageFile().write(encoded);
305 this.getLogger().trace("EXIT!"); //NOI18N
309 * Adds given contact to list
311 * @param instance An instance of FrameworkInterface to add
312 * @param list List instance
314 private void addToList (final Storeable instance, final List<Storeable> list) {
316 this.getLogger().trace(MessageFormat.format("contact={0} - CALLED!", instance)); //NOI18N
319 if (instance == null) {
321 throw new NullPointerException("contact is null"); //NOI18N
322 } else if (list == null) {
324 throw new NullPointerException("list is null"); //NOI18N
328 this.getLogger().debug(MessageFormat.format("contact={0}", instance)); //NOI18N
330 // Is the contact read?
331 if (instance instanceof FrameworkInterface) {
333 boolean added = list.add(instance);
336 this.getLogger().debug(MessageFormat.format("contact={0} added={1}", instance, added)); //NOI18N
338 // Has it been added?
341 this.getLogger().warn("Contact object has not been added."); //NOI18N
346 this.getLogger().trace("EXIT!"); //NOI18N
350 * Returns storage file
352 * @return Storage file instance
354 private RandomAccessFile getStorageFile () {
355 return this.storageFile;
359 * Checks whether end of file has been reached
361 * @return Whether lines are left to read
363 private boolean isEndOfFile () {
365 boolean isEof = true;
368 isEof = (this.getStorageFile().getFilePointer() >= this.length());
369 } catch (final IOException ex) {
370 // Length cannot be determined
371 this.getLogger().catching(ex);
375 this.getLogger().trace(MessageFormat.format("isEof={0} : EXIT!", isEof)); //NOI18N
380 * Parses given line and creates a Contact instance
382 * @param line Raw line to parse
384 * @return Contact instance
386 private Contact parseLineToContact (final String line) throws BadTokenException {
388 this.getLogger().trace(MessageFormat.format("line={0} - CALLED!", line)); //NOI18N
390 // Init A lot variables
393 Gender gender = null;
395 Contact contact = null;
398 this.getLogger().debug(MessageFormat.format("line={0}", line)); //NOI18N
401 // @TODO Move this into separate method
402 StringTokenizer tokenizer = new StringTokenizer(line, ";"); //NOI18N
408 // The tokens are now available, so get all
409 while (tokenizer.hasMoreElements()) {
415 String token = tokenizer.nextToken();
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);
424 this.getLogger().debug(MessageFormat.format("token={0}", token)); //NOI18N
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
432 // All fine, so remove it
433 String strippedToken = token.substring(1, token.length() - 1);
435 // Is the string's content "null"?
436 if (strippedToken.equals("null")) { //NOI18N
438 this.getLogger().debug(MessageFormat.format("strippedToken={0} - NULL!", strippedToken)); //NOI18N
440 // This needs to be set to null
441 strippedToken = null;
445 this.getLogger().debug(MessageFormat.format("strippedToken={0}", strippedToken)); //NOI18N
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?
451 num = Long.valueOf(strippedToken);
454 this.getLogger().debug(MessageFormat.format("strippedToken={0} - NUMBER!", strippedToken)); //NOI18N
455 } catch (final NumberFormatException ex) {
456 // No number, then set default
461 // Now, let's try a boolean check, if no null
462 if ((strippedToken != null) && (num == null) && ((strippedToken.equals("true")) || (strippedToken.equals("false")))) { //NOI18N
464 this.getLogger().debug(MessageFormat.format("strippedToken={0} - BOOLEAN!", strippedToken)); //NOI18N
466 // parseBoolean() is relaxed, so no exceptions
467 bool = Boolean.valueOf(strippedToken);
471 this.getLogger().debug(MessageFormat.format("strippedToken={0},num={1},bool={2}", strippedToken, num, bool)); //NOI18N
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));
479 this.getLogger().debug(MessageFormat.format("strippedToken={0},gender={1}", strippedToken, gender)); //NOI18N
481 // This instance must be there
482 assert (gender instanceof Gender) : MessageFormat.format("gender is not set by Gender.fromChar({0})", strippedToken); //NOI18N
485 // Now it depends on the counter which position we need to check
487 case 0: // isOwnContact
488 assert ((bool instanceof Boolean));
491 this.getLogger().debug(MessageFormat.format("bool={0}", bool)); //NOI18N
493 // Is it own contact?
496 this.getLogger().debug("Creating UserContact object ..."); //NOI18N
499 contact = new UserContact();
502 this.getLogger().debug("Creating BookContact object ..."); //NOI18N
505 contact = new BookContact();
510 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
513 contact.updateNameData(gender, null, null, null);
517 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
518 assert (gender instanceof Gender) : "gender instance is not set"; //NOI18N
521 contact.updateNameData(gender, strippedToken, null, null);
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
529 contact.updateNameData(gender, null, strippedToken, null);
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
537 contact.updateNameData(gender, null, null, strippedToken);
540 case 5: // Street number
541 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
544 contact.updateAddressData(strippedToken, 0, null, null);
548 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
551 contact.updateAddressData(null, num, null, null);
555 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
558 contact.updateAddressData(null, 0, strippedToken, null);
561 case 8: // Country code
562 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
565 contact.updateAddressData(null, 0, null, strippedToken);
568 case 9: // Phone number
569 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
572 contact.updateOtherData(strippedToken, null, null, null, null, null);
575 case 10: // Fax number
576 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
579 contact.updateOtherData(null, strippedToken, null, null, null, null);
582 case 11: // Cellphone number
583 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
586 contact.updateOtherData(null, null, strippedToken, null, null, null);
589 case 12: // Email address
590 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
593 contact.updateOtherData(null, null, null, strippedToken, null, null);
597 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
600 contact.updateOtherData(null, null, null, null, strippedToken, null);
604 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
607 contact.updateOtherData(null, null, null, null, null, strippedToken);
610 default: // New data entry
611 this.getLogger().warn(MessageFormat.format("Will not handle unknown data {0} at index {1}", strippedToken, count)); //NOI18N
615 // Increment counter for next round
620 this.getLogger().trace(MessageFormat.format("contact={0} - EXIT!", contact)); //NOI18N
622 // Return finished instance
627 * Reads a line from file base
629 * @return Read line from file
631 private String readLine () {
633 this.getLogger().trace("CALLED!"); //NOI18N
640 String base64 = this.getStorageFile().readLine();
643 byte[] decoded = Base64.getDecoder().decode(base64);
646 input = new String(decoded);
647 } catch (final IOException ex) {
648 this.getLogger().catching(ex);
652 this.getLogger().trace(MessageFormat.format("input={0} - EXIT!", input)); //NOI18N
654 // Return read string or null
659 * Reads the database file, if available, and adds all read lines into the
662 * @return A list with Contact instances
664 private List<? extends Storeable> readList () throws BadTokenException {
665 this.getLogger().trace("CALLED!"); //NOI18N
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);
674 this.getLogger().debug(MessageFormat.format("lines={0}", lines)); //NOI18N
677 // @TODO The maximum length could be guessed from file size?
678 List<Storeable> list = new ArrayList<>(lines);
682 Storeable instance = null;
685 while (!this.isEndOfFile()) {
687 line = this.readLine();
690 instance = (Storeable) this.parseLineToContact(line);
692 // The contact instance should be there now
693 assert (instance instanceof FrameworkInterface) : MessageFormat.format("instance is not set: {0}", instance); //NOI18N
696 this.addToList(instance, list);
699 // Return finished list
700 this.getLogger().trace(MessageFormat.format("list.size()={0} : EXIT!", list.size())); //NOI18N