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.file.DatabaseFile;
33 import org.mxchange.jcore.database.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 {
50 private final String fileName;
53 * Output stream for this storage engine
55 private final SynchronizeableFile storageFile;
58 * Constructor with table name
60 * @param frontend Wrapper instance to call back
61 * @throws java.io.FileNotFoundException If the file was not found
63 public Base64CsvDatabaseBackend (final DatabaseFrontend frontend) throws FileNotFoundException {
65 this.getLogger().trace(MessageFormat.format("frontend={0} - CALLED!", frontend)); //NOI18N
68 String tableName = frontend.getTableName();
71 this.getLogger().debug(MessageFormat.format("Trying to initialize table {0} ...", tableName)); //NOI18N
73 // Set table name here, too
74 this.setTableName(tableName);
77 this.setFrontend(frontend);
79 // Construct file name
80 this.fileName = String.format("%s/table_%s.b64", this.getProperty("database.backend.storagepath"), tableName); //NOI18N
83 this.getLogger().debug(MessageFormat.format("Trying to open file {0} ...", this.fileName)); //NOI18N
86 // Try to initialize the storage (file instance)
87 this.storageFile = new DatabaseFile(this.fileName); //NOI18N
88 } catch (final FileNotFoundException ex) {
90 this.getLogger().error(MessageFormat.format("File {0} cannot be opened: {1}", this.fileName, ex.toString())); //NOI18N
95 this.getLogger().debug(MessageFormat.format("Database for {0} has been initialized.", tableName)); //NOI18N
99 * This database backend does not need to connect
102 public void connectToDatabase () {
107 public Result<? extends Storeable> doInsertDataSet (final Map<String, Object> dataset) throws IOException {
109 this.getLogger().trace(MessageFormat.format("dataset={0} - CALLED!", dataset)); //NOI18N
111 // dataset should not be null and not empty
112 if (dataset == null) {
113 // It is null, so abort here
114 throw new NullPointerException("dataset is null"); //NOI18N
115 } else if (dataset.isEmpty()) {
116 // It is empty, also abort here
117 throw new IllegalArgumentException("dataset is empty"); //NOI18N
121 this.getLogger().debug(MessageFormat.format("Need to parse {0} values ...", dataset.size())); //NOI18N
123 // Get iterator from it
124 Iterator<Map.Entry<String, Object>> iterator = dataset.entrySet().iterator();
126 // Full output string
127 StringBuilder output = new StringBuilder(dataset.size() * 20);
130 output.append(String.format("key=%s,value=\"%s\";", this.getFrontend().getIdName(), this.getTotalRows() + 1)); //NOI18N
132 // "Walk" over all entries
133 while (iterator.hasNext()) {
135 Map.Entry<String, Object> entry = iterator.next();
138 Object value = entry.getValue();
140 // Validate value, should not contain "
141 if (value instanceof String) {
142 // Is String so cast ist
143 String str = (String) value;
145 // Does it contain a " ?
146 if (str.contains("\"")) { //NOI18N
148 throw new IllegalArgumentException(MessageFormat.format("value {0} with double-quote not supported yet.", value)); //NOI18N
152 // Generate key=value pair
153 String pair = String.format("key=%s,value=\"%s\";", entry.getKey(), String.valueOf(value)); //NOI18N
156 this.getLogger().debug(MessageFormat.format("pair={0}", pair)); //NOI18N
162 // Then write it to file
163 this.writeData(output);
165 // The result set needs to be transformed into Result, so initialize a result instance here
166 Result<? extends Storeable> result = new DatabaseResult();
169 this.getLogger().trace(MessageFormat.format("result={0} - EXIT!", result)); //NOI18N
176 public Result<? extends Storeable> doSelectByCriteria (final SearchableCriteria critera) throws IOException, BadTokenException, CorruptedDatabaseFileException, NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
178 this.getLogger().trace(MessageFormat.format("criteria={0} - CALLED!", critera)); //NOI18N
180 // Init result instance
181 Result<? extends Storeable> result = new DatabaseResult();
183 // First rewind this backend
186 // Then loop over all rows until the end has reached
187 while (!this.isEndOfFile()) {
189 String line = this.readLine();
192 this.getLogger().debug(MessageFormat.format("line={0}", line)); //NOI18N
194 // Parse it to a Map<String, Object>
195 Map<String, String> map = this.getMapFromLine(line);
197 // Convert it to a Storeable instance
198 Storeable storeable = this.getFrontend().toStoreable(map);
201 this.getLogger().debug(MessageFormat.format("storeable={0}", storeable)); //NOI18N
203 // Now matches the found instance
204 if (critera.matches(storeable)) {
205 // Then add it to result
206 result.add(storeable);
215 * Shuts down this backend
218 public void doShutdown () throws IOException {
220 this.getLogger().trace("CALLED!"); //NOI18N
223 this.getStorageFile().close();
226 this.getLogger().trace("EXIT!"); //NOI18N
230 public final int getTotalRows () throws IOException {
232 this.getLogger().trace("CALLED!"); //NOI18N
240 // Walk through all rows
241 while (!this.isEndOfFile()) {
243 String line = this.readLine();
246 this.getLogger().debug(MessageFormat.format("line={0}", line)); //NOI18N
253 this.getLogger().trace(MessageFormat.format("count={0} - EXIT!", count)); //NOI18N
260 * Tries to interpret the given decoded line and puts its key/value pairs into a map.
262 * @param line Decoded line from database file
263 * @return A Map with keys and values from line
264 * @throws org.mxchange.jcore.exceptions.CorruptedDatabaseFileException If the file is believed damaged
265 * @throws org.mxchange.jcore.exceptions.BadTokenException If a bad token was found
267 private Map<String, String> getMapFromLine (final String line) throws CorruptedDatabaseFileException, BadTokenException {
269 this.getLogger().debug(MessageFormat.format("line={0} - CALLED!", line)); //NOI18N
271 // "line" must not be null or empty
274 throw new NullPointerException("line is null"); //NOI18N
275 } else if (line.isEmpty()) {
277 throw new IllegalArgumentException("line is empty, maybe isEndOfFile() was not called?"); //NOI18N
278 } else if (!line.endsWith(";")) { //NOI18N
280 throw new CorruptedDatabaseFileException(this.fileName, "No semicolon at end of line"); //NOI18N
281 } else if (!line.contains("key=")) { //NOI18N
283 throw new CorruptedDatabaseFileException(this.fileName, "No \"key=bla\" found."); //NOI18N
284 } else if (!line.contains("value=")) { //NOI18N
286 throw new CorruptedDatabaseFileException(this.fileName, "No \"value=bla\" found."); //NOI18N
289 Pattern pattern = Pattern.compile("(key=([a-z0-9_]{1,}),value=\"([^\"]*)\";){1,}"); //NOI18N
290 Matcher matcher = pattern.matcher(line);
293 this.getLogger().debug(MessageFormat.format("matches={0}", matcher.matches())); //NOI18N
296 if (!matcher.matches()) {
297 // Corrupted file found
298 throw new CorruptedDatabaseFileException(this.fileName, MessageFormat.format("line {0} doesn't match regular expression.", line)); //NOI18N
302 Map<String, String> map = new HashMap<>(line.length() / 40);
304 pattern = Pattern.compile("(key=([a-z0-9_]{1,}),value=\"([^\"]*)\";)"); //NOI18N
305 matcher = pattern.matcher(line);
311 while (matcher.find(init)) {
313 String match = matcher.group(1);
314 String key = matcher.group(2);
315 String value = matcher.group(3);
317 // key must noch be empty
318 assert((key != null) && (!key.isEmpty())) : MessageFormat.format("key={0} is not valid", key); //NOI18N
321 int start = matcher.start();
322 int end = matcher.end();
325 this.getLogger().debug(MessageFormat.format("init={0},start={1},end={2},match={3},key={4},value={5}", init, start, end, match, key, value)); //NOI18N
327 // Add key/value to map
335 this.getLogger().trace(MessageFormat.format("map()={0} - EXIT!", map.size())); //NOI18N
337 // Return finished map
342 * Returns storage file
344 * @return Storage file instance
346 private SynchronizeableFile getStorageFile () {
347 return this.storageFile;
351 * Checks whether end of file has been reached
353 * @return Whether lines are left to read
355 private boolean isEndOfFile () {
357 boolean isEof = true;
360 isEof = (this.getStorageFile().getFilePointer() >= this.length());
361 } catch (final IOException ex) {
362 // Length cannot be determined
363 this.getLogger().catching(ex);
367 this.getLogger().trace(MessageFormat.format("isEof={0} : EXIT!", isEof)); //NOI18N
372 * Get length of underlaying file
374 * @return Length of underlaying file
376 private long length () throws IOException {
379 // Try to get length from file
380 length = this.getStorageFile().length();
381 this.getLogger().debug(MessageFormat.format("length={0}", length)); //NOI18N
384 this.getLogger().trace(MessageFormat.format("length={0} : EXIT!", length)); //NOI18N
389 * Reads a line from file base
391 * @return Read line from file
393 private String readLine () {
395 this.getLogger().trace("CALLED!"); //NOI18N
402 String base64 = this.getStorageFile().readLine();
405 if (base64 == null) {
406 // Then throw NPE here
407 throw new NullPointerException("base64 is null, maybe missed to call isEndOfFile() ?"); //NOI18N
411 byte[] decoded = Base64.decodeBase64(base64);
414 input = new String(decoded).trim();
415 } catch (final IOException ex) {
416 this.getLogger().catching(ex);
420 this.getLogger().trace(MessageFormat.format("input={0} - EXIT!", input)); //NOI18N
422 // Return read string or null
429 private void rewind () throws IOException {
431 this.getLogger().trace("CALLED!"); //NOI18N
433 // Rewind underlaying database file
434 this.getStorageFile().seek(0);
437 this.getLogger().trace("EXIT!"); //NOI18N
441 * Writes a line with BASE64 encoding to database file
443 * @param output Output string to write
445 private void writeData (final StringBuilder output) throws IOException {
447 this.getLogger().trace(MessageFormat.format("output={0} - CALLED!", output)); //NOI18N
449 // No null or empty strings
450 if (output == null) {
452 throw new NullPointerException("output is null"); //NOI18N
453 } else if (output.length() == 0) {
455 throw new IllegalArgumentException("output is empty"); //NOI18N
458 // Encode it to BASE64
459 String rawOutput = Base64.encodeBase64String(output.toString().getBytes());
462 this.getLogger().debug(MessageFormat.format("rawOutput={0}", rawOutput)); //NOI18N
464 // Write each line separately
465 this.getStorageFile().writeBytes(String.format("%s\n", rawOutput)); //NOI18N
468 this.getLogger().trace("EXIT!"); //NOI18N