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