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