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 org.mxchange.addressbook.FrameworkInterface;
30 import org.mxchange.addressbook.contact.Contact;
31 import org.mxchange.addressbook.database.backend.BaseDatabaseBackend;
32 import org.mxchange.addressbook.database.backend.DatabaseBackend;
33 import org.mxchange.addressbook.database.frontend.DatabaseFrontend;
34 import org.mxchange.addressbook.database.storage.Storeable;
35 import org.mxchange.addressbook.database.storage.csv.StoreableCsv;
36 import org.mxchange.addressbook.exceptions.BadTokenException;
39 * A database backend with CSV file as storage implementation
41 * @author Roland Haeder
43 public class Base64CsvDatabaseBackend extends BaseDatabaseBackend implements DatabaseBackend {
46 * Output stream for this storage engine
48 private RandomAccessFile storageFile;
51 * Constructor with table name
53 * @param tableName Name of "table"
54 * @param wrapper Wrapper instance to call back
56 public Base64CsvDatabaseBackend (final String tableName, final DatabaseFrontend wrapper) {
58 this.getLogger().trace(MessageFormat.format("tableName={0},wrapper={1}", tableName, wrapper)); //NOI18N
61 this.getLogger().debug(MessageFormat.format("Trying to initialize table {0} ...", tableName)); //NOI18N
63 // Set table name here, too
64 this.setTableName(tableName);
67 this.setWrapper(wrapper);
69 // Construct file name
70 String fileName = String.format("data/table_%s.b64", tableName); //NOI18N
73 this.getLogger().debug(MessageFormat.format("Trying to open file {0} ...", fileName)); //NOI18N
76 // Try to initialize the storage (file instance)
77 this.storageFile = new RandomAccessFile(fileName, "rw"); //NOI18N
78 } catch (final FileNotFoundException ex) {
80 this.getLogger().error(MessageFormat.format("File {0} cannot be opened: {1}", fileName, ex.toString())); //NOI18N
85 this.getLogger().debug(MessageFormat.format("Database for {0} has been initialized.", tableName)); //NOI18N
89 * This database backend does not need to connect
92 public void connectToDatabase () throws SQLException {
97 * Shuts down this backend
100 public void doShutdown () {
102 this.getLogger().trace("CALLED!"); //NOI18N
106 this.getStorageFile().close();
107 } catch (final IOException ex) {
109 this.abortProgramWithException(ex);
113 this.getLogger().trace("EXIT!"); //NOI18N
117 * Some "getter" for row index from given boolean row value
119 * @param columnName Name of column
120 * @param bool Boolean value to look for
124 public int getRowIndexFromColumn (final String columnName, final boolean bool) {
126 this.getLogger().trace(MessageFormat.format("columnName={0},bool={1} - CALLED!", columnName, bool)); //NOI18N
128 // Row indexes start with zero
134 // Try to find the proper row
135 while (!this.isEndOfFile()) {
137 String line = this.readLine();
140 this.getLogger().debug(MessageFormat.format("line={0}", line));
142 // Callback the wrapper to handle parsing
144 Storeable storeable = this.getWrapper().parseLineToStoreable(line);
147 this.getLogger().debug(MessageFormat.format("storeable={0}", storeable));
150 if (storeable.isValueEqual(columnName, bool)) {
152 this.getLogger().debug(MessageFormat.format("Found storeable={0} for columnName={1} and bool={2}", storeable, columnName, bool));
157 } catch (final BadTokenException ex) {
159 this.abortProgramWithException(ex);
171 * Some "getter" for total row count
173 * @return Total row count
176 public int getTotalCount () {
178 this.getLogger().trace("CALLED!"); //NOI18N
181 // Do a deprecated call
182 // @todo this needs rewrite!
183 return this.readList().size();
184 } catch (final BadTokenException ex) {
185 this.abortProgramWithException(ex);
189 this.getLogger().trace("Returning -1 ... : EXIT!"); //NOI18N
194 * Checks whether at least one row is found with given boolean value.
196 * @param columnName Column to check for boolean value
197 * @param bool Boolean value to check
198 * @return Whether boolean value is found and returns at least one row
201 public boolean isRowFound (final String columnName, final boolean bool) {
203 this.getLogger().trace(MessageFormat.format("columnName={0},bool={1} - CALLED!", columnName, bool)); //NOI18N
205 // Is at least one entry found?
206 if (this.getTotalCount() == 0) {
207 // No entry found at all
211 // Default is not found
212 boolean isFound = false;
217 // Then loop through all lines
218 while (!this.isEndOfFile()) {
220 String line = this.readLine();
223 this.getLogger().debug(MessageFormat.format("line={0}", line));
226 // And parse it to a Contact instance
227 Contact contact = (Contact) this.getWrapper().parseLineToStoreable(line);
230 this.getLogger().debug(MessageFormat.format("contact={0}", contact));
232 // This should not be null
233 if (contact == null) {
235 throw new NullPointerException("contact is null");
238 // Now let the contact object check if it has such attribute
239 if (contact.isValueEqual(columnName, bool)) {
244 } catch (final BadTokenException ex) {
245 // Don't continue with bad data
246 this.abortProgramWithException(ex);
251 this.getLogger().trace(MessageFormat.format("isFound={0} - EXIT!", isFound));
258 * Gets an iterator for contacts
260 * @return Iterator for contacts
261 * @throws org.mxchange.addressbook.exceptions.BadTokenException If the
262 * underlaying method has found an invalid token
265 public Iterator<? extends Storeable> iterator () throws BadTokenException {
267 this.getLogger().trace("CALLED!"); //NOI18N
270 * Then read the file into RAM (yes, not perfect for >1000 entries ...)
271 * and get a List back.
273 List<? extends Storeable> list = this.readList();
276 assert (list instanceof List) : "list has not been set."; //NOI18N
279 this.getLogger().trace(MessageFormat.format("list.iterator()={0} - EXIT!", list.iterator())); //NOI18N
281 // Get iterator from list and return it
282 return list.iterator();
286 * Get length of underlaying file
288 * @return Length of underlaying file
291 public long length () {
295 length = this.getStorageFile().length();
296 this.getLogger().debug(MessageFormat.format("length={0}", length)); //NOI18N
297 } catch (final IOException ex) {
298 // Length cannot be determined
300 this.abortProgramWithException(ex);
304 this.getLogger().trace(MessageFormat.format("length={0} : EXIT!", length)); //NOI18N
309 * Reads a single row from database.
311 * @param rowIndex Row index (or how much to skip)
312 * @return A Storeable instance
313 * @throws org.mxchange.addressbook.exceptions.BadTokenException If a token
314 * was badly formatted
317 public Storeable readRow (final int rowIndex) throws BadTokenException {
321 // Intialize variables
323 Storeable storeable = null;
326 while (!this.isEndOfFile() || (count < rowIndex)) {
328 String line = this.readLine();
331 this.getLogger().debug(MessageFormat.format("line={0}", line));
333 // Callback the wrapper to handle parsing
334 storeable = this.getWrapper().parseLineToStoreable(line);
337 this.getLogger().debug(MessageFormat.format("storeable={0}", storeable));
344 this.getLogger().trace(MessageFormat.format("storeable={0} - EXIT!", storeable));
345 // Return found element
353 public void rewind () {
355 this.getLogger().trace("CALLED!"); //NOI18N
358 // Rewind underlaying database file
359 this.getStorageFile().seek(0);
360 } catch (final IOException ex) {
362 this.abortProgramWithException(ex);
366 this.getLogger().trace("EXIT!"); //NOI18N
370 * Stores given object by "visiting" it
372 * @param object An object implementing Storeable
373 * @throws java.io.IOException From "inner" class
376 public void store (final Storeable object) throws IOException {
378 this.getLogger().trace(MessageFormat.format("object={0} - CALLED!", object)); //NOI18N
380 // Object must not be null
381 if (object == null) {
383 throw new NullPointerException("object is null"); //NOI18N
386 // Make sure the instance is there (DataOutput flawor)
387 assert (this.storageFile instanceof DataOutput);
389 // Try to cast it, this will fail if the interface is not implemented
390 StoreableCsv csv = (StoreableCsv) object;
392 // Now get a string from the object that needs to be stored
393 String str = csv.getCsvStringFromStoreableObject();
396 this.getLogger().debug(MessageFormat.format("str({0})={1}", str.length(), str)); //NOI18N
398 // Encode line in BASE-64
399 byte[] encoded = Base64.getEncoder().encode(str.getBytes());
401 // The string is now a valid CSV string
402 this.getStorageFile().write(encoded);
405 this.getLogger().trace("EXIT!"); //NOI18N
409 * Adds given contact to list
411 * @param instance An instance of FrameworkInterface to add
412 * @param list List instance
414 private void addToList (final Storeable instance, final List<Storeable> list) {
416 this.getLogger().trace(MessageFormat.format("contact={0} - CALLED!", instance)); //NOI18N
419 if (instance == null) {
421 throw new NullPointerException("contact is null"); //NOI18N
422 } else if (list == null) {
424 throw new NullPointerException("list is null"); //NOI18N
428 this.getLogger().debug(MessageFormat.format("contact={0}", instance)); //NOI18N
430 // Is the contact read?
431 if (instance instanceof FrameworkInterface) {
433 boolean added = list.add(instance);
436 this.getLogger().debug(MessageFormat.format("contact={0} added={1}", instance, added)); //NOI18N
438 // Has it been added?
441 this.getLogger().warn("Contact object has not been added."); //NOI18N
446 this.getLogger().trace("EXIT!"); //NOI18N
450 * Returns storage file
452 * @return Storage file instance
454 private RandomAccessFile getStorageFile () {
455 return this.storageFile;
459 * Checks whether end of file has been reached
461 * @return Whether lines are left to read
463 private boolean isEndOfFile () {
465 boolean isEof = true;
468 isEof = (this.getStorageFile().getFilePointer() >= this.length());
469 } catch (final IOException ex) {
470 // Length cannot be determined
471 this.getLogger().catching(ex);
475 this.getLogger().trace(MessageFormat.format("isEof={0} : EXIT!", isEof)); //NOI18N
480 * Reads a line from file base
482 * @return Read line from file
484 private String readLine () {
486 this.getLogger().trace("CALLED!"); //NOI18N
493 String base64 = this.getStorageFile().readLine();
496 if (base64 == null) {
497 // Then throw NPE here
498 throw new NullPointerException("base64 is null");
502 byte[] decoded = Base64.getDecoder().decode(base64);
505 input = new String(decoded);
506 } catch (final IOException ex) {
507 this.getLogger().catching(ex);
511 this.getLogger().trace(MessageFormat.format("input={0} - EXIT!", input)); //NOI18N
513 // Return read string or null
518 * Reads the database file, if available, and adds all read lines into the
521 * @return A list with Contact instances
523 private List<? extends Storeable> readList () throws BadTokenException {
524 this.getLogger().trace("CALLED!"); //NOI18N
529 // Get file size and divide it by 140 (possible average length of one line)
530 int lines = Math.round(this.length() / 140 + 0.5f);
533 this.getLogger().debug(MessageFormat.format("lines={0}", lines)); //NOI18N
536 // @TODO The maximum length could be guessed from file size?
537 List<Storeable> list = new ArrayList<>(lines);
541 Storeable instance = null;
544 while (!this.isEndOfFile()) {
546 line = this.readLine();
549 instance = this.getWrapper().parseLineToStoreable(line);
551 // The contact instance should be there now
552 assert (instance instanceof FrameworkInterface) : MessageFormat.format("instance is not set: {0}", instance); //NOI18N
555 this.addToList(instance, list);
558 // Return finished list
559 this.getLogger().trace(MessageFormat.format("list.size()={0} : EXIT!", list.size())); //NOI18N