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.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;
41 * A database backend with CSV file as storage implementation
43 * @author Roland Haeder
45 public class Base64CsvDatabaseBackend extends BaseDatabaseBackend implements DatabaseBackend {
48 * Output stream for this storage engine
50 private RandomAccessFile storageFile;
53 * Constructor with table name
55 * @param tableName Name of "table"
57 public Base64CsvDatabaseBackend (final String tableName) {
59 this.getLogger().debug(MessageFormat.format("Trying to initialize table {0} ...", tableName)); //NOI18N
61 // Set table name here, too
62 this.setTableName(tableName);
64 // Construct file name
65 String fileName = String.format("data/table_%s.b64", tableName); //NOI18N
68 this.getLogger().debug(MessageFormat.format("Trying to open file {0} ...", fileName)); //NOI18N
71 // Try to initialize the storage (file instance)
72 this.storageFile = new RandomAccessFile(fileName, "rw"); //NOI18N
73 } catch (final FileNotFoundException ex) {
75 this.getLogger().error(MessageFormat.format("File {0} cannot be opened: {1}", fileName, ex.toString())); //NOI18N
80 this.getLogger().debug(MessageFormat.format("Database for {0} has been initialized.", tableName)); //NOI18N
84 * This database backend does not need to connect
87 public void connectToDatabase () throws SQLException {
92 * Gets an iterator for contacts
94 * @return Iterator for contacts
95 * @throws org.mxchange.addressbook.exceptions.BadTokenException If the
96 * underlaying method has found an invalid token
100 public Iterator<Contact> contactIterator () throws BadTokenException {
102 this.getLogger().trace("CALLED!"); //NOI18N
105 * Then read the file into RAM (yes, not perfect for >1000 entries ...)
106 * and get a List back.
108 List<Contact> list = this.readContactList();
111 assert (list instanceof List) : "list has not been set."; //NOI18N
114 this.getLogger().trace(MessageFormat.format("list.iterator()={0} - EXIT!", list.iterator())); //NOI18N
116 // Get iterator from list and return it
117 return list.iterator();
121 * Shuts down this backend
124 public void doShutdown () {
126 this.getLogger().trace("CALLED!"); //NOI18N
130 this.getStorageFile().close();
131 } catch (final IOException ex) {
133 this.abortProgramWithException(ex);
137 this.getLogger().trace("EXIT!"); //NOI18N
141 * Some "getter" for total row count
143 * @return Total row count
146 public int getTotalCount () {
148 this.getLogger().trace("CALLED!"); //NOI18N
151 // Do a deprecated call
152 // @todo this needs rewrite!
153 return this.readContactList().size();
154 } catch (final BadTokenException ex) {
155 this.abortProgramWithException(ex);
159 this.getLogger().trace("Returning -1 ... : EXIT!"); //NOI18N
164 * Checks whether at least one row is found with given boolean value.
166 * @param columnName Column to check for boolean value
167 * @param bool Boolean value to check
168 * @return Whether boolean value is found and returns at least one row
171 public boolean isRowFound (final String columnName, final boolean bool) {
173 this.getLogger().trace(MessageFormat.format("columnName={0},bool={1} - CALLED!", columnName, bool)); //NOI18N
175 // Is at least one entry found?
176 if (this.getTotalCount() == 0) {
177 // No entry found at all
181 // Default is not found
182 boolean isFound = false;
187 // Then loop through all lines
188 while (!this.isEndOfFile()) {
190 String line = this.readLine();
193 this.getLogger().debug(MessageFormat.format("line={0}", line));
196 // And parse it to a Contact instance
197 Contact contact = this.parseLineToContact(line);
200 this.getLogger().debug(MessageFormat.format("contact={0}", contact));
202 // This should not be null
203 if (contact == null) {
205 throw new NullPointerException("contact is null");
208 // Now let the contact object check if it has such attribute
209 if (contact.isValueEqual(columnName, bool)) {
214 } catch (final BadTokenException ex) {
215 // Don't continue with bad data
216 this.abortProgramWithException(ex);
221 this.getLogger().trace(MessageFormat.format("isFound={0} - EXIT!", isFound));
228 * Get length of underlaying file
230 * @return Length of underlaying file
233 public long length () {
237 length = this.getStorageFile().length();
238 this.getLogger().debug(MessageFormat.format("length={0}", length)); //NOI18N
239 } catch (final IOException ex) {
240 // Length cannot be determined
242 this.abortProgramWithException(ex);
246 this.getLogger().trace(MessageFormat.format("length={0} : EXIT!", length)); //NOI18N
254 public void rewind () {
256 this.getLogger().trace("CALLED!"); //NOI18N
259 // Rewind underlaying database file
260 this.getStorageFile().seek(0);
261 } catch (final IOException ex) {
263 this.abortProgramWithException(ex);
267 this.getLogger().trace("EXIT!"); //NOI18N
271 * Stores given object by "visiting" it
273 * @param object An object implementing Storeable
274 * @throws java.io.IOException From "inner" class
277 public void store (final Storeable object) throws IOException {
279 this.getLogger().trace(MessageFormat.format("object={0} - CALLED!", object)); //NOI18N
281 // Object must not be null
282 if (object == null) {
284 throw new NullPointerException("object is null"); //NOI18N
287 // Make sure the instance is there (DataOutput flawor)
288 assert (this.storageFile instanceof DataOutput);
290 // Try to cast it, this will fail if the interface is not implemented
291 StoreableCsv csv = (StoreableCsv) object;
293 // Now get a string from the object that needs to be stored
294 String str = csv.getCsvStringFromStoreableObject();
297 this.getLogger().debug(MessageFormat.format("str({0})={1}", str.length(), str)); //NOI18N
299 // Encode line in BASE-64
300 byte[] encoded = Base64.getEncoder().encode(str.getBytes());
302 // The string is now a valid CSV string
303 this.getStorageFile().write(encoded);
306 this.getLogger().trace("EXIT!"); //NOI18N
310 * Adds given contact to list
312 * @param contact Contact instance to add
313 * @param list List instance
315 private void addContactToList (final Contact contact, final List<Contact> list) {
317 this.getLogger().trace(MessageFormat.format("contact={0} - CALLED!", contact)); //NOI18N
320 if (contact == null) {
322 throw new NullPointerException("contact is null"); //NOI18N
323 } else if (list == null) {
325 throw new NullPointerException("list is null"); //NOI18N
329 this.getLogger().debug(MessageFormat.format("contact={0}", contact)); //NOI18N
331 // Is the contact read?
332 if (contact instanceof Contact) {
334 boolean added = list.add(contact);
337 this.getLogger().debug(MessageFormat.format("contact={0} added={1}", contact, added)); //NOI18N
339 // Has it been added?
342 this.getLogger().warn("Contact object has not been added."); //NOI18N
347 this.getLogger().trace("EXIT!"); //NOI18N
351 * Returns storage file
353 * @return Storage file instance
355 private RandomAccessFile getStorageFile () {
356 return this.storageFile;
360 * Checks whether end of file has been reached
362 * @return Whether lines are left to read
364 private boolean isEndOfFile () {
366 boolean isEof = true;
369 isEof = (this.getStorageFile().getFilePointer() >= this.length());
370 } catch (final IOException ex) {
371 // Length cannot be determined
372 this.getLogger().catching(ex);
376 this.getLogger().trace(MessageFormat.format("isEof={0} : EXIT!", isEof)); //NOI18N
381 * Parses given line and creates a Contact instance
383 * @param line Raw line to parse
385 * @return Contact instance
387 private Contact parseLineToContact (final String line) throws BadTokenException {
389 this.getLogger().trace(MessageFormat.format("line={0} - CALLED!", line)); //NOI18N
391 // Init A lot variables
394 Gender gender = null;
396 Contact contact = null;
399 this.getLogger().debug(MessageFormat.format("line={0}", line)); //NOI18N
402 // @TODO Move this into separate method
403 StringTokenizer tokenizer = new StringTokenizer(line, ";"); //NOI18N
409 // The tokens are now available, so get all
410 while (tokenizer.hasMoreElements()) {
416 String token = tokenizer.nextToken();
418 // If char " is at pos 2 (0,1,2), then cut it of there
419 if ((token.charAt(0) != '"') && (token.charAt(2) == '"')) {
420 // UTF-8 writer characters found
421 token = token.substring(2);
425 this.getLogger().debug(MessageFormat.format("token={0}", token)); //NOI18N
427 // Verify token, it must have double-quotes on each side
428 if ((!token.startsWith("\"")) || (!token.endsWith("\""))) { //NOI18N
429 // Something bad was read
430 throw new BadTokenException(token, count); //NOI18N
433 // All fine, so remove it
434 String strippedToken = token.substring(1, token.length() - 1);
436 // Is the string's content "null"?
437 if (strippedToken.equals("null")) { //NOI18N
439 this.getLogger().debug(MessageFormat.format("strippedToken={0} - NULL!", strippedToken)); //NOI18N
441 // This needs to be set to null
442 strippedToken = null;
446 this.getLogger().debug(MessageFormat.format("strippedToken={0}", strippedToken)); //NOI18N
448 // Now, let's try a number check, if no null
449 if (strippedToken != null) {
450 // Okay, no null, maybe the string bears a decimal number?
452 num = Long.valueOf(strippedToken);
455 this.getLogger().debug(MessageFormat.format("strippedToken={0} - NUMBER!", strippedToken)); //NOI18N
456 } catch (final NumberFormatException ex) {
457 // No number, then set default
462 // Now, let's try a boolean check, if no null
463 if ((strippedToken != null) && (num == null) && ((strippedToken.equals("true")) || (strippedToken.equals("false")))) { //NOI18N
465 this.getLogger().debug(MessageFormat.format("strippedToken={0} - BOOLEAN!", strippedToken)); //NOI18N
467 // parseBoolean() is relaxed, so no exceptions
468 bool = Boolean.valueOf(strippedToken);
472 this.getLogger().debug(MessageFormat.format("strippedToken={0},num={1},bool={2}", strippedToken, num, bool)); //NOI18N
474 // Now, let's try a gender check, if no null
475 if ((strippedToken != null) && (num == null) && (bool == null) && ((strippedToken.equals("M")) || (strippedToken.equals("F")) || (strippedToken.equals("C")))) { //NOI18N
476 // Get first character
477 gender = Gender.fromChar(strippedToken.charAt(0));
480 this.getLogger().debug(MessageFormat.format("strippedToken={0},gender={1}", strippedToken, gender)); //NOI18N
482 // This instance must be there
483 assert (gender instanceof Gender) : MessageFormat.format("gender is not set by Gender.fromChar({0})", strippedToken); //NOI18N
486 // Now it depends on the counter which position we need to check
488 case 0: // isOwnContact
489 assert ((bool instanceof Boolean));
492 this.getLogger().debug(MessageFormat.format("bool={0}", bool)); //NOI18N
494 // Is it own contact?
497 this.getLogger().debug("Creating UserContact object ..."); //NOI18N
500 contact = new UserContact();
503 this.getLogger().debug("Creating BookContact object ..."); //NOI18N
506 contact = new BookContact();
511 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
514 contact.updateNameData(gender, null, null, null);
518 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
519 assert (gender instanceof Gender) : "gender instance is not set"; //NOI18N
522 contact.updateNameData(gender, strippedToken, null, null);
525 case 3: // Family name
526 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
527 assert (gender instanceof Gender) : "gender instance is not set"; //NOI18N
530 contact.updateNameData(gender, null, strippedToken, null);
533 case 4: // Company name
534 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
535 assert (gender instanceof Gender) : "gender instance is not set"; //NOI18N
538 contact.updateNameData(gender, null, null, strippedToken);
541 case 5: // Street number
542 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
545 contact.updateAddressData(strippedToken, 0, null, null);
549 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
552 contact.updateAddressData(null, num, null, null);
556 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
559 contact.updateAddressData(null, 0, strippedToken, null);
562 case 8: // Country code
563 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
566 contact.updateAddressData(null, 0, null, strippedToken);
569 case 9: // Phone number
570 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
573 contact.updateOtherData(strippedToken, null, null, null, null, null);
576 case 10: // Fax number
577 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
580 contact.updateOtherData(null, strippedToken, null, null, null, null);
583 case 11: // Cellphone number
584 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
587 contact.updateOtherData(null, null, strippedToken, null, null, null);
590 case 12: // Email address
591 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
594 contact.updateOtherData(null, null, null, strippedToken, null, null);
598 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
601 contact.updateOtherData(null, null, null, null, strippedToken, null);
605 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
608 contact.updateOtherData(null, null, null, null, null, strippedToken);
611 default: // New data entry
612 this.getLogger().warn(MessageFormat.format("Will not handle unknown data {0} at index {1}", strippedToken, count)); //NOI18N
616 // Increment counter for next round
621 this.getLogger().trace(MessageFormat.format("contact={0} - EXIT!", contact)); //NOI18N
623 // Return finished instance
628 * Reads the database file, if available, and adds all read lines into the
631 * @return A list with Contact instances
632 * @deprecated Is to much "contacts" specific
635 private List<Contact> readContactList () throws BadTokenException {
636 this.getLogger().trace("CALLED!"); //NOI18N
641 // Get file size and divide it by 140 (possible average length of one line)
642 int lines = Math.round(this.length() / 140 + 0.5f);
645 this.getLogger().debug(MessageFormat.format("lines={0}", lines)); //NOI18N
648 // @TODO The maximum length could be guessed from file size?
649 List<Contact> list = new ArrayList<>(lines);
653 Contact contact = null;
656 while (!this.isEndOfFile()) {
658 line = this.readLine();
661 contact = this.parseLineToContact(line);
663 // The contact instance should be there now
664 assert (contact instanceof Contact) : MessageFormat.format("contact is not set: {0}", contact); //NOI18N
667 this.addContactToList(contact, list);
670 // Return finished list
671 this.getLogger().trace(MessageFormat.format("list.size()={0} : EXIT!", list.size())); //NOI18N
676 * Reads a line from file base
678 * @return Read line from file
680 private String readLine () {
682 this.getLogger().trace("CALLED!"); //NOI18N
689 String base64 = this.getStorageFile().readLine();
692 byte[] decoded = Base64.getDecoder().decode(base64);
695 input = new String(decoded);
696 } catch (final IOException ex) {
697 this.getLogger().catching(ex);
701 this.getLogger().trace(MessageFormat.format("input={0} - EXIT!", input)); //NOI18N
703 // Return read string or null