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