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.jcore.database.backend.base64;
19 import java.io.FileNotFoundException;
20 import java.io.IOException;
21 import java.lang.reflect.InvocationTargetException;
22 import java.text.MessageFormat;
23 import java.util.HashMap;
24 import java.util.Iterator;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28 import org.apache.commons.codec.binary.Base64;
29 import org.mxchange.jcore.criteria.searchable.SearchableCriteria;
30 import org.mxchange.jcore.database.backend.BaseDatabaseBackend;
31 import org.mxchange.jcore.database.backend.DatabaseBackend;
32 import org.mxchange.jcore.database.backend.file.DatabaseFile;
33 import org.mxchange.jcore.database.backend.file.SynchronizeableFile;
34 import org.mxchange.jcore.database.frontend.DatabaseFrontend;
35 import org.mxchange.jcore.database.result.DatabaseResult;
36 import org.mxchange.jcore.database.result.Result;
37 import org.mxchange.jcore.database.storage.Storeable;
38 import org.mxchange.jcore.exceptions.BadTokenException;
39 import org.mxchange.jcore.exceptions.CorruptedDatabaseFileException;
42 * A database backend with CSV file as storage implementation
44 * @author Roland Haeder
46 public class Base64CsvDatabaseBackend extends BaseDatabaseBackend implements DatabaseBackend {
49 * Output stream for this storage engine
51 private final SynchronizeableFile storageFile;
56 private final String fileName;
59 * Constructor with table name
61 * @param frontend Wrapper instance to call back
62 * @throws java.io.FileNotFoundException If the file was not found
64 public Base64CsvDatabaseBackend (final DatabaseFrontend frontend) throws FileNotFoundException {
66 this.getLogger().trace(MessageFormat.format("frontend={0} - CALLED!", frontend)); //NOI18N
69 String tableName = frontend.getTableName();
72 this.getLogger().debug(MessageFormat.format("Trying to initialize table {0} ...", tableName)); //NOI18N
74 // Set table name here, too
75 this.setTableName(tableName);
78 this.setFrontend(frontend);
80 // Construct file name
81 this.fileName = String.format("%s/table_%s.b64", this.getProperty("database.backend.storagepath"), tableName); //NOI18N
84 this.getLogger().debug(MessageFormat.format("Trying to open file {0} ...", this.fileName)); //NOI18N
87 // Try to initialize the storage (file instance)
88 this.storageFile = new DatabaseFile(this.fileName); //NOI18N
89 } catch (final FileNotFoundException ex) {
91 this.getLogger().error(MessageFormat.format("File {0} cannot be opened: {1}", this.fileName, ex.toString())); //NOI18N
96 this.getLogger().debug(MessageFormat.format("Database for {0} has been initialized.", tableName)); //NOI18N
100 * This database backend does not need to connect
103 public void connectToDatabase () {
108 public Result<? extends Storeable> doInsertDataSet (final Map<String, Object> dataset) throws IOException {
110 this.getLogger().trace(MessageFormat.format("dataset={0} - CALLED!", dataset)); //NOI18N
112 // dataset should not be null and not empty
113 if (dataset == null) {
114 // It is null, so abort here
115 throw new NullPointerException("dataset is null"); //NOI18N
116 } else if (dataset.isEmpty()) {
117 // It is empty, also abort here
118 throw new IllegalArgumentException("dataset is empty"); //NOI18N
122 this.getLogger().debug(MessageFormat.format("Need to parse {0} values ...", dataset.size())); //NOI18N
124 // Get iterator from it
125 Iterator<Map.Entry<String, Object>> iterator = dataset.entrySet().iterator();
127 // Full output string
128 StringBuilder output = new StringBuilder(dataset.size() * 20);
131 output.append(String.format("key=%s,value=\"%s\";", this.getFrontend().getIdName(), this.getTotalRows() + 1)); //NOI18N
133 // "Walk" over all entries
134 while (iterator.hasNext()) {
136 Map.Entry<String, Object> entry = iterator.next();
139 Object value = entry.getValue();
141 // Validate value, should not contain "
142 if (value instanceof String) {
143 // Is String so cast ist
144 String str = (String) value;
146 // Does it contain a " ?
147 if (str.contains("\"")) { //NOI18N
149 throw new IllegalArgumentException(MessageFormat.format("value {0} with double-quote not supported yet.", value)); //NOI18N
153 // Generate key=value pair
154 String pair = String.format("key=%s,value=\"%s\";", entry.getKey(), String.valueOf(value)); //NOI18N
157 this.getLogger().debug(MessageFormat.format("pair={0}", pair)); //NOI18N
163 // Then write it to file
164 this.writeData(output);
166 // The result set needs to be transformed into Result, so initialize a result instance here
167 Result<? extends Storeable> result = new DatabaseResult();
170 this.getLogger().trace(MessageFormat.format("result={0} - EXIT!", result)); //NOI18N
177 public Result<? extends Storeable> doSelectByCriteria (final SearchableCriteria critera) throws IOException, BadTokenException, CorruptedDatabaseFileException, NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
179 this.getLogger().trace(MessageFormat.format("criteria={0} - CALLED!", critera)); //NOI18N
181 // Init result instance
182 Result<? extends Storeable> result = new DatabaseResult();
184 // First rewind this backend
187 // Then loop over all rows until the end has reached
188 while (!this.isEndOfFile()) {
190 String line = this.readLine();
193 this.getLogger().debug(MessageFormat.format("line={0}", line)); //NOI18N
195 // Parse it to a Map<String, Object>
196 Map<String, String> map = this.getMapFromLine(line);
198 // Convert it to a Storeable instance
199 Storeable storeable = this.getFrontend().toStoreable(map);
202 this.getLogger().debug(MessageFormat.format("storeable={0}", storeable)); //NOI18N
204 // Now matches the found instance
205 if (critera.matches(storeable)) {
206 // Then add it to result
207 result.add(storeable);
216 * Shuts down this backend
219 public void doShutdown () throws IOException {
221 this.getLogger().trace("CALLED!"); //NOI18N
224 this.getStorageFile().close();
227 this.getLogger().trace("EXIT!"); //NOI18N
231 * Returns storage file
233 * @return Storage file instance
235 private SynchronizeableFile getStorageFile () {
236 return this.storageFile;
240 * Checks whether end of file has been reached
242 * @return Whether lines are left to read
244 private boolean isEndOfFile () {
246 boolean isEof = true;
249 isEof = (this.getStorageFile().getFilePointer() >= this.length());
250 } catch (final IOException ex) {
251 // Length cannot be determined
252 this.getLogger().catching(ex);
256 this.getLogger().trace(MessageFormat.format("isEof={0} : EXIT!", isEof)); //NOI18N
261 * Get length of underlaying file
263 * @return Length of underlaying file
265 private long length () throws IOException {
268 // Try to get length from file
269 length = this.getStorageFile().length();
270 this.getLogger().debug(MessageFormat.format("length={0}", length)); //NOI18N
273 this.getLogger().trace(MessageFormat.format("length={0} : EXIT!", length)); //NOI18N
278 * Reads a line from file base
280 * @return Read line from file
282 private String readLine () {
284 this.getLogger().trace("CALLED!"); //NOI18N
291 String base64 = this.getStorageFile().readLine();
294 if (base64 == null) {
295 // Then throw NPE here
296 throw new NullPointerException("base64 is null, maybe missed to call isEndOfFile() ?"); //NOI18N
300 byte[] decoded = Base64.decodeBase64(base64);
303 input = new String(decoded).trim();
304 } catch (final IOException ex) {
305 this.getLogger().catching(ex);
309 this.getLogger().trace(MessageFormat.format("input={0} - EXIT!", input)); //NOI18N
311 // Return read string or null
318 private void rewind () throws IOException {
320 this.getLogger().trace("CALLED!"); //NOI18N
322 // Rewind underlaying database file
323 this.getStorageFile().seek(0);
326 this.getLogger().trace("EXIT!"); //NOI18N
330 * Writes a line with BASE64 encoding to database file
332 * @param output Output string to write
334 private void writeData (final StringBuilder output) throws IOException {
336 this.getLogger().trace(MessageFormat.format("output={0} - CALLED!", output)); //NOI18N
338 // No null or empty strings
339 if (output == null) {
341 throw new NullPointerException("output is null"); //NOI18N
342 } else if (output.length() == 0) {
344 throw new IllegalArgumentException("output is empty"); //NOI18N
347 // Encode it to BASE64
348 String rawOutput = Base64.encodeBase64String(output.toString().getBytes());
351 this.getLogger().debug(MessageFormat.format("rawOutput={0}", rawOutput)); //NOI18N
353 // Write each line separately
354 this.getStorageFile().writeBytes(String.format("%s\n", rawOutput)); //NOI18N
357 this.getLogger().trace("EXIT!"); //NOI18N
361 * Tries to interpret the given decoded line and puts its key/value pairs into a map.
363 * @param line Decoded line from database file
364 * @return A Map with keys and values from line
365 * @throws org.mxchange.jcore.exceptions.CorruptedDatabaseFileException If the file is believed damaged
366 * @throws org.mxchange.jcore.exceptions.BadTokenException If a bad token was found
368 private Map<String, String> getMapFromLine (final String line) throws CorruptedDatabaseFileException, BadTokenException {
370 this.getLogger().debug(MessageFormat.format("line={0} - CALLED!", line)); //NOI18N
372 // "line" must not be null or empty
375 throw new NullPointerException("line is null"); //NOI18N
376 } else if (line.isEmpty()) {
378 throw new IllegalArgumentException("line is empty, maybe isEndOfFile() was not called?"); //NOI18N
379 } else if (!line.endsWith(";")) { //NOI18N
381 throw new CorruptedDatabaseFileException(this.fileName, "No semicolon at end of line"); //NOI18N
382 } else if (!line.contains("key=")) { //NOI18N
384 throw new CorruptedDatabaseFileException(this.fileName, "No \"key=bla\" found."); //NOI18N
385 } else if (!line.contains("value=")) { //NOI18N
387 throw new CorruptedDatabaseFileException(this.fileName, "No \"value=bla\" found."); //NOI18N
390 Pattern pattern = Pattern.compile("(key=([a-z0-9_]{1,}),value=\"([^\"]*)\";){1,}"); //NOI18N
391 Matcher matcher = pattern.matcher(line);
394 this.getLogger().debug(MessageFormat.format("matches={0}", matcher.matches())); //NOI18N
397 if (!matcher.matches()) {
398 // Corrupted file found
399 throw new CorruptedDatabaseFileException(this.fileName, MessageFormat.format("line {0} doesn't match regular expression.", line)); //NOI18N
403 Map<String, String> map = new HashMap<>(line.length() / 40);
405 pattern = Pattern.compile("(key=([a-z0-9_]{1,}),value=\"([^\"]*)\";)"); //NOI18N
406 matcher = pattern.matcher(line);
412 while (matcher.find(init)) {
414 String match = matcher.group(1);
415 String key = matcher.group(2);
416 String value = matcher.group(3);
418 // key must noch be empty
419 assert((key != null) && (!key.isEmpty())) : MessageFormat.format("key={0} is not valid", key); //NOI18N
422 int start = matcher.start();
423 int end = matcher.end();
426 this.getLogger().debug(MessageFormat.format("init={0},start={1},end={2},match={3},key={4},value={5}", init, start, end, match, key, value)); //NOI18N
428 // Add key/value to map
436 this.getLogger().trace(MessageFormat.format("map()={0} - EXIT!", map.size())); //NOI18N
438 // Return finished map
443 public final Long getTotalRows () throws IOException {
445 this.getLogger().trace("CALLED!"); //NOI18N
453 // Walk through all rows
454 while (!this.isEndOfFile()) {
456 String line = this.readLine();
459 this.getLogger().debug(MessageFormat.format("line={0}", line)); //NOI18N
466 this.getLogger().trace(MessageFormat.format("count={0} - EXIT!", count)); //NOI18N