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.frontend.DatabaseWrapper;
38 import org.mxchange.addressbook.database.storage.Storeable;
39 import org.mxchange.addressbook.database.storage.csv.StoreableCsv;
40 import org.mxchange.addressbook.exceptions.BadTokenException;
43 * A database backend with CSV file as storage implementation
45 * @author Roland Haeder
47 public class Base64CsvDatabaseBackend extends BaseDatabaseBackend implements DatabaseBackend {
50 * Output stream for this storage engine
52 private RandomAccessFile storageFile;
55 * Constructor with table name
57 * @param tableName Name of "table"
58 * @param wrapper Wrapper instance to call back
60 public Base64CsvDatabaseBackend (final String tableName, final DatabaseWrapper wrapper) {
62 this.getLogger().trace(MessageFormat.format("tableName={0},wrapper={1}", tableName, wrapper)); //NOI18N
65 this.getLogger().debug(MessageFormat.format("Trying to initialize table {0} ...", tableName)); //NOI18N
67 // Set table name here, too
68 this.setTableName(tableName);
71 this.setWrapper(wrapper);
73 // Construct file name
74 String fileName = String.format("data/table_%s.b64", tableName); //NOI18N
77 this.getLogger().debug(MessageFormat.format("Trying to open file {0} ...", fileName)); //NOI18N
80 // Try to initialize the storage (file instance)
81 this.storageFile = new RandomAccessFile(fileName, "rw"); //NOI18N
82 } catch (final FileNotFoundException ex) {
84 this.getLogger().error(MessageFormat.format("File {0} cannot be opened: {1}", fileName, ex.toString())); //NOI18N
89 this.getLogger().debug(MessageFormat.format("Database for {0} has been initialized.", tableName)); //NOI18N
93 * This database backend does not need to connect
96 public void connectToDatabase () throws SQLException {
101 * Shuts down this backend
104 public void doShutdown () {
106 this.getLogger().trace("CALLED!"); //NOI18N
110 this.getStorageFile().close();
111 } catch (final IOException ex) {
113 this.abortProgramWithException(ex);
117 this.getLogger().trace("EXIT!"); //NOI18N
121 * Some "getter" for total row count
123 * @return Total row count
126 public int getTotalCount () {
128 this.getLogger().trace("CALLED!"); //NOI18N
131 // Do a deprecated call
132 // @todo this needs rewrite!
133 return this.readList().size();
134 } catch (final BadTokenException ex) {
135 this.abortProgramWithException(ex);
139 this.getLogger().trace("Returning -1 ... : EXIT!"); //NOI18N
144 * Checks whether at least one row is found with given boolean value.
146 * @param columnName Column to check for boolean value
147 * @param bool Boolean value to check
148 * @return Whether boolean value is found and returns at least one row
151 public boolean isRowFound (final String columnName, final boolean bool) {
153 this.getLogger().trace(MessageFormat.format("columnName={0},bool={1} - CALLED!", columnName, bool)); //NOI18N
155 // Is at least one entry found?
156 if (this.getTotalCount() == 0) {
157 // No entry found at all
161 // Default is not found
162 boolean isFound = false;
167 // Then loop through all lines
168 while (!this.isEndOfFile()) {
170 String line = this.readLine();
173 this.getLogger().debug(MessageFormat.format("line={0}", line));
176 // And parse it to a Contact instance
177 Contact contact = this.parseLineToContact(line);
180 this.getLogger().debug(MessageFormat.format("contact={0}", contact));
182 // This should not be null
183 if (contact == null) {
185 throw new NullPointerException("contact is null");
188 // Now let the contact object check if it has such attribute
189 if (contact.isValueEqual(columnName, bool)) {
194 } catch (final BadTokenException ex) {
195 // Don't continue with bad data
196 this.abortProgramWithException(ex);
201 this.getLogger().trace(MessageFormat.format("isFound={0} - EXIT!", isFound));
208 * Gets an iterator for contacts
210 * @return Iterator for contacts
211 * @throws org.mxchange.addressbook.exceptions.BadTokenException If the underlaying method has found an invalid token
214 public Iterator<? extends Storeable> iterator () throws BadTokenException {
216 this.getLogger().trace("CALLED!"); //NOI18N
219 * Then read the file into RAM (yes, not perfect for >1000 entries ...)
220 * and get a List back.
222 List<? extends Storeable> list = this.readList();
225 assert (list instanceof List) : "list has not been set."; //NOI18N
228 this.getLogger().trace(MessageFormat.format("list.iterator()={0} - EXIT!", list.iterator())); //NOI18N
230 // Get iterator from list and return it
231 return list.iterator();
235 * Get length of underlaying file
237 * @return Length of underlaying file
240 public long length () {
244 length = this.getStorageFile().length();
245 this.getLogger().debug(MessageFormat.format("length={0}", length)); //NOI18N
246 } catch (final IOException ex) {
247 // Length cannot be determined
249 this.abortProgramWithException(ex);
253 this.getLogger().trace(MessageFormat.format("length={0} : EXIT!", length)); //NOI18N
258 * Reads a single row from database.
260 * @param rowIndex Row index (or how much to skip)
261 * @return A Storeable instance
264 public Storeable readRow (final int rowIndex) {
268 // Intialize variables
270 Storeable storeable = null;
275 String line = this.readLine();
277 // Callback the wrapper to handle parsing
278 storeable = this.getWrapper().parseLineToStoreable(line);
282 } while (!this.isEndOfFile() || (count > rowIndex));
284 // Return found element
292 public void rewind () {
294 this.getLogger().trace("CALLED!"); //NOI18N
297 // Rewind underlaying database file
298 this.getStorageFile().seek(0);
299 } catch (final IOException ex) {
301 this.abortProgramWithException(ex);
305 this.getLogger().trace("EXIT!"); //NOI18N
309 * Stores given object by "visiting" it
311 * @param object An object implementing Storeable
312 * @throws java.io.IOException From "inner" class
315 public void store (final Storeable object) throws IOException {
317 this.getLogger().trace(MessageFormat.format("object={0} - CALLED!", object)); //NOI18N
319 // Object must not be null
320 if (object == null) {
322 throw new NullPointerException("object is null"); //NOI18N
325 // Make sure the instance is there (DataOutput flawor)
326 assert (this.storageFile instanceof DataOutput);
328 // Try to cast it, this will fail if the interface is not implemented
329 StoreableCsv csv = (StoreableCsv) object;
331 // Now get a string from the object that needs to be stored
332 String str = csv.getCsvStringFromStoreableObject();
335 this.getLogger().debug(MessageFormat.format("str({0})={1}", str.length(), str)); //NOI18N
337 // Encode line in BASE-64
338 byte[] encoded = Base64.getEncoder().encode(str.getBytes());
340 // The string is now a valid CSV string
341 this.getStorageFile().write(encoded);
344 this.getLogger().trace("EXIT!"); //NOI18N
348 * Adds given contact to list
350 * @param instance An instance of FrameworkInterface to add
351 * @param list List instance
353 private void addToList (final Storeable instance, final List<Storeable> list) {
355 this.getLogger().trace(MessageFormat.format("contact={0} - CALLED!", instance)); //NOI18N
358 if (instance == null) {
360 throw new NullPointerException("contact is null"); //NOI18N
361 } else if (list == null) {
363 throw new NullPointerException("list is null"); //NOI18N
367 this.getLogger().debug(MessageFormat.format("contact={0}", instance)); //NOI18N
369 // Is the contact read?
370 if (instance instanceof FrameworkInterface) {
372 boolean added = list.add(instance);
375 this.getLogger().debug(MessageFormat.format("contact={0} added={1}", instance, added)); //NOI18N
377 // Has it been added?
380 this.getLogger().warn("Contact object has not been added."); //NOI18N
385 this.getLogger().trace("EXIT!"); //NOI18N
389 * Returns storage file
391 * @return Storage file instance
393 private RandomAccessFile getStorageFile () {
394 return this.storageFile;
398 * Checks whether end of file has been reached
400 * @return Whether lines are left to read
402 private boolean isEndOfFile () {
404 boolean isEof = true;
407 isEof = (this.getStorageFile().getFilePointer() >= this.length());
408 } catch (final IOException ex) {
409 // Length cannot be determined
410 this.getLogger().catching(ex);
414 this.getLogger().trace(MessageFormat.format("isEof={0} : EXIT!", isEof)); //NOI18N
419 * Parses given line and creates a Contact instance
421 * @param line Raw line to parse
423 * @return Contact instance
425 private Contact parseLineToContact (final String line) throws BadTokenException {
427 this.getLogger().trace(MessageFormat.format("line={0} - CALLED!", line)); //NOI18N
429 // Init A lot variables
432 Gender gender = null;
434 Contact contact = null;
437 this.getLogger().debug(MessageFormat.format("line={0}", line)); //NOI18N
440 // @TODO Move this into separate method
441 StringTokenizer tokenizer = new StringTokenizer(line, ";"); //NOI18N
447 // The tokens are now available, so get all
448 while (tokenizer.hasMoreElements()) {
454 String token = tokenizer.nextToken();
456 // If char " is at pos 2 (0,1,2), then cut it of there
457 if ((token.charAt(0) != '"') && (token.charAt(2) == '"')) {
458 // UTF-8 writer characters found
459 token = token.substring(2);
463 this.getLogger().debug(MessageFormat.format("token={0}", token)); //NOI18N
465 // Verify token, it must have double-quotes on each side
466 if ((!token.startsWith("\"")) || (!token.endsWith("\""))) { //NOI18N
467 // Something bad was read
468 throw new BadTokenException(token, count); //NOI18N
471 // All fine, so remove it
472 String strippedToken = token.substring(1, token.length() - 1);
474 // Is the string's content "null"?
475 if (strippedToken.equals("null")) { //NOI18N
477 this.getLogger().debug(MessageFormat.format("strippedToken={0} - NULL!", strippedToken)); //NOI18N
479 // This needs to be set to null
480 strippedToken = null;
484 this.getLogger().debug(MessageFormat.format("strippedToken={0}", strippedToken)); //NOI18N
486 // Now, let's try a number check, if no null
487 if (strippedToken != null) {
488 // Okay, no null, maybe the string bears a decimal number?
490 num = Long.valueOf(strippedToken);
493 this.getLogger().debug(MessageFormat.format("strippedToken={0} - NUMBER!", strippedToken)); //NOI18N
494 } catch (final NumberFormatException ex) {
495 // No number, then set default
500 // Now, let's try a boolean check, if no null
501 if ((strippedToken != null) && (num == null) && ((strippedToken.equals("true")) || (strippedToken.equals("false")))) { //NOI18N
503 this.getLogger().debug(MessageFormat.format("strippedToken={0} - BOOLEAN!", strippedToken)); //NOI18N
505 // parseBoolean() is relaxed, so no exceptions
506 bool = Boolean.valueOf(strippedToken);
510 this.getLogger().debug(MessageFormat.format("strippedToken={0},num={1},bool={2}", strippedToken, num, bool)); //NOI18N
512 // Now, let's try a gender check, if no null
513 if ((strippedToken != null) && (num == null) && (bool == null) && ((strippedToken.equals("M")) || (strippedToken.equals("F")) || (strippedToken.equals("C")))) { //NOI18N
514 // Get first character
515 gender = Gender.fromChar(strippedToken.charAt(0));
518 this.getLogger().debug(MessageFormat.format("strippedToken={0},gender={1}", strippedToken, gender)); //NOI18N
520 // This instance must be there
521 assert (gender instanceof Gender) : MessageFormat.format("gender is not set by Gender.fromChar({0})", strippedToken); //NOI18N
524 // Now it depends on the counter which position we need to check
526 case 0: // isOwnContact
527 assert ((bool instanceof Boolean));
530 this.getLogger().debug(MessageFormat.format("bool={0}", bool)); //NOI18N
532 // Is it own contact?
535 this.getLogger().debug("Creating UserContact object ..."); //NOI18N
538 contact = new UserContact();
541 this.getLogger().debug("Creating BookContact object ..."); //NOI18N
544 contact = new BookContact();
549 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
552 contact.updateNameData(gender, null, null, null);
556 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
557 assert (gender instanceof Gender) : "gender instance is not set"; //NOI18N
560 contact.updateNameData(gender, strippedToken, null, null);
563 case 3: // Family name
564 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
565 assert (gender instanceof Gender) : "gender instance is not set"; //NOI18N
568 contact.updateNameData(gender, null, strippedToken, null);
571 case 4: // Company name
572 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
573 assert (gender instanceof Gender) : "gender instance is not set"; //NOI18N
576 contact.updateNameData(gender, null, null, strippedToken);
579 case 5: // Street number
580 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
583 contact.updateAddressData(strippedToken, 0, null, null);
587 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
590 contact.updateAddressData(null, num, null, null);
594 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
597 contact.updateAddressData(null, 0, strippedToken, null);
600 case 8: // Country code
601 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
604 contact.updateAddressData(null, 0, null, strippedToken);
607 case 9: // Phone number
608 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
611 contact.updateOtherData(strippedToken, null, null, null, null, null);
614 case 10: // Fax number
615 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
618 contact.updateOtherData(null, strippedToken, null, null, null, null);
621 case 11: // Cellphone number
622 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
625 contact.updateOtherData(null, null, strippedToken, null, null, null);
628 case 12: // Email address
629 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
632 contact.updateOtherData(null, null, null, strippedToken, null, null);
636 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
639 contact.updateOtherData(null, null, null, null, strippedToken, null);
643 assert (contact instanceof Contact) : "First token was not boolean"; //NOI18N
646 contact.updateOtherData(null, null, null, null, null, strippedToken);
649 default: // New data entry
650 this.getLogger().warn(MessageFormat.format("Will not handle unknown data {0} at index {1}", strippedToken, count)); //NOI18N
654 // Increment counter for next round
659 this.getLogger().trace(MessageFormat.format("contact={0} - EXIT!", contact)); //NOI18N
661 // Return finished instance
666 * Reads a line from file base
668 * @return Read line from file
670 private String readLine () {
672 this.getLogger().trace("CALLED!"); //NOI18N
679 String base64 = this.getStorageFile().readLine();
682 byte[] decoded = Base64.getDecoder().decode(base64);
685 input = new String(decoded);
686 } catch (final IOException ex) {
687 this.getLogger().catching(ex);
691 this.getLogger().trace(MessageFormat.format("input={0} - EXIT!", input)); //NOI18N
693 // Return read string or null
698 * Reads the database file, if available, and adds all read lines into the
701 * @return A list with Contact instances
703 private List<? extends Storeable> readList () throws BadTokenException {
704 this.getLogger().trace("CALLED!"); //NOI18N
709 // Get file size and divide it by 140 (possible average length of one line)
710 int lines = Math.round(this.length() / 140 + 0.5f);
713 this.getLogger().debug(MessageFormat.format("lines={0}", lines)); //NOI18N
716 // @TODO The maximum length could be guessed from file size?
717 List<Storeable> list = new ArrayList<>(lines);
721 Storeable instance = null;
724 while (!this.isEndOfFile()) {
726 line = this.readLine();
729 instance = (Storeable) this.parseLineToContact(line);
731 // The contact instance should be there now
732 assert (instance instanceof FrameworkInterface) : MessageFormat.format("instance is not set: {0}", instance); //NOI18N
735 this.addToList(instance, list);
738 // Return finished list
739 this.getLogger().trace(MessageFormat.format("list.size()={0} : EXIT!", list.size())); //NOI18N