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