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 org.mxchange.jcore.database.backend.file.DatabaseFile;
20 import java.io.FileNotFoundException;
21 import java.io.IOException;
22 import java.io.RandomAccessFile;
23 import java.lang.reflect.InvocationTargetException;
24 import java.text.MessageFormat;
25 import java.util.HashMap;
26 import java.util.Iterator;
28 import java.util.regex.Matcher;
29 import java.util.regex.Pattern;
30 import org.apache.commons.codec.binary.Base64;
31 import org.mxchange.jcore.criteria.searchable.SearchableCriteria;
32 import org.mxchange.jcore.database.backend.BaseDatabaseBackend;
33 import org.mxchange.jcore.database.backend.DatabaseBackend;
34 import org.mxchange.jcore.database.backend.file.SynchronizeableFile;
35 import org.mxchange.jcore.database.frontend.DatabaseFrontend;
36 import org.mxchange.jcore.database.result.DatabaseResult;
37 import org.mxchange.jcore.database.result.Result;
38 import org.mxchange.jcore.database.storage.Storeable;
39 import org.mxchange.jcore.exceptions.BadTokenException;
40 import org.mxchange.jcore.exceptions.CorruptedDatabaseFileException;
43 * A database backend with CSV file as storage implementation
45 * @author Roland Haeder
47 public class Base64CsvDatabaseBackend extends BaseDatabaseBackend implements DatabaseBackend {
50 * Output stream for this storage engine
52 private final SynchronizeableFile storageFile;
57 private final String fileName;
60 * Constructor with table name
62 * @param frontend Wrapper instance to call back
63 * @throws java.io.FileNotFoundException If the file was not found
65 public Base64CsvDatabaseBackend (final DatabaseFrontend frontend) throws FileNotFoundException {
67 this.getLogger().trace(MessageFormat.format("frontend={0} - CALLED!", frontend)); //NOI18N
70 String tableName = frontend.getTableName();
73 this.getLogger().debug(MessageFormat.format("Trying to initialize table {0} ...", tableName)); //NOI18N
75 // Set table name here, too
76 this.setTableName(tableName);
79 this.setFrontend(frontend);
81 // Construct file name
82 this.fileName = String.format("%s/table_%s.b64", this.getProperty("database.backend.storagepath"), tableName); //NOI18N
85 this.getLogger().debug(MessageFormat.format("Trying to open file {0} ...", this.fileName)); //NOI18N
88 // Try to initialize the storage (file instance)
89 this.storageFile = new DatabaseFile(this.fileName); //NOI18N
90 } catch (final FileNotFoundException ex) {
92 this.getLogger().error(MessageFormat.format("File {0} cannot be opened: {1}", this.fileName, ex.toString())); //NOI18N
97 this.getLogger().debug(MessageFormat.format("Database for {0} has been initialized.", tableName)); //NOI18N
101 * This database backend does not need to connect
104 public void connectToDatabase () {
109 public Result<? extends Storeable> doInsertDataSet (final Map<String, Object> dataset) throws IOException {
111 this.getLogger().trace(MessageFormat.format("dataset={0} - CALLED!", dataset));
113 // dataset should not be null and not empty
114 if (dataset == null) {
115 // It is null, so abort here
116 throw new NullPointerException("dataset is null");
117 } else if (dataset.isEmpty()) {
118 // It is empty, also abort here
119 throw new IllegalArgumentException("dataset is empty");
123 this.getLogger().debug(MessageFormat.format("Need to parse {0} values ...", dataset.size()));
125 // Get iterator from it
126 Iterator<Map.Entry<String, Object>> iterator = dataset.entrySet().iterator();
128 // Full output string
129 StringBuilder output = new StringBuilder(dataset.size() * 20);
132 output.append(String.format("key=%s,value=\"%s\";", this.getFrontend().getIdName(), this.getTotalRows() + 1));
134 // "Walk" over all entries
135 while (iterator.hasNext()) {
137 Map.Entry<String, Object> entry = iterator.next();
140 Object value = entry.getValue();
142 // Validate value, should not contain "
143 if (value instanceof String) {
144 // Is String so cast ist
145 String str = (String) value;
147 // Does it contain a " ?
148 if (str.contains("\"")) {
150 throw new IllegalArgumentException("value " + value + " with double-quote not supported yet.");
154 // Generate key=value pair
155 String pair = String.format("key=%s,value=\"%s\";", entry.getKey(), String.valueOf(value));
158 this.getLogger().debug("pair=" + pair);
164 // Then write it to file
165 this.writeData(output);
167 // The result set needs to be transformed into Result, so initialize a result instance here
168 Result<? extends Storeable> result = new DatabaseResult();
171 this.getLogger().trace(MessageFormat.format("result={0} - EXIT!", result));
178 public Result<? extends Storeable> doSelectByCriteria (final SearchableCriteria critera) throws IOException, BadTokenException, CorruptedDatabaseFileException, NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
180 this.getLogger().trace(MessageFormat.format("criteria={0} - CALLED!", critera));
182 // Init result instance
183 Result<Storeable> result = new DatabaseResult();
185 // First rewind this backend
188 // Then loop over all rows until the end has reached
189 while (!this.isEndOfFile()) {
191 String line = this.readLine();
194 this.getLogger().debug(MessageFormat.format("line={0}", line));
196 // Parse it to a Map<String, Object>
197 Map<String, String> map = this.getMapFromLine(line);
199 // Convert it to a Storeable instance
200 Storeable storeable = this.getFrontend().toStoreable(map);
203 this.getLogger().debug(MessageFormat.format("storeable={0}", storeable));
205 // Now matches the found instance
206 if (critera.matches(storeable)) {
207 // Then add it to result
208 result.add(storeable);
217 * Shuts down this backend
220 public void doShutdown () throws IOException {
222 this.getLogger().trace("CALLED!"); //NOI18N
225 this.getStorageFile().close();
228 this.getLogger().trace("EXIT!"); //NOI18N
232 * Returns storage file
234 * @return Storage file instance
236 private SynchronizeableFile getStorageFile () {
237 return this.storageFile;
241 * Checks whether end of file has been reached
243 * @return Whether lines are left to read
245 private boolean isEndOfFile () {
247 boolean isEof = true;
250 isEof = (this.getStorageFile().getFilePointer() >= this.length());
251 } catch (final IOException ex) {
252 // Length cannot be determined
253 this.getLogger().catching(ex);
257 this.getLogger().trace(MessageFormat.format("isEof={0} : EXIT!", isEof)); //NOI18N
262 * Get length of underlaying file
264 * @return Length of underlaying file
266 private long length () throws IOException {
269 // Try to get length from file
270 length = this.getStorageFile().length();
271 this.getLogger().debug(MessageFormat.format("length={0}", length)); //NOI18N
274 this.getLogger().trace(MessageFormat.format("length={0} : EXIT!", length)); //NOI18N
279 * Reads a line from file base
281 * @return Read line from file
283 private String readLine () {
285 this.getLogger().trace("CALLED!"); //NOI18N
292 String base64 = this.getStorageFile().readLine();
295 if (base64 == null) {
296 // Then throw NPE here
297 throw new NullPointerException("base64 is null, maybe missed to call isEndOfFile() ?"); //NOI18N
301 byte[] decoded = Base64.decodeBase64(base64);
304 input = new String(decoded).trim();
305 } catch (final IOException ex) {
306 this.getLogger().catching(ex);
310 this.getLogger().trace(MessageFormat.format("input={0} - EXIT!", input)); //NOI18N
312 // Return read string or null
319 private void rewind () throws IOException {
321 this.getLogger().trace("CALLED!"); //NOI18N
323 // Rewind underlaying database file
324 this.getStorageFile().seek(0);
327 this.getLogger().trace("EXIT!"); //NOI18N
331 * Writes a line with BASE64 encoding to database file
333 * @param output Output string to write
335 private void writeData (final StringBuilder output) throws IOException {
337 this.getLogger().trace("output=" + output + " - CALLED!");
339 // No null or empty strings
340 if (output == null) {
342 throw new NullPointerException("output is null");
343 } else if (output.length() == 0) {
345 throw new IllegalArgumentException("output is empty");
348 // Encode it to BASE64
349 String rawOutput = Base64.encodeBase64String(output.toString().getBytes());
352 this.getLogger().debug("rawOutput=" + rawOutput);
354 // Write each line separately
355 this.getStorageFile().writeBytes(rawOutput + "\n");
358 this.getLogger().trace("EXIT!");
362 * Tries to interpret the given decoded line and puts its key/value pairs into a map.
364 * @param line Decoded line from database file
365 * @return A Map with keys and values from line
366 * @throws org.mxchange.jcore.exceptions.CorruptedDatabaseFileException If the file is believed damaged
367 * @throws org.mxchange.jcore.exceptions.BadTokenException If a bad token was found
369 private Map<String, String> getMapFromLine (final String line) throws CorruptedDatabaseFileException, BadTokenException {
371 this.getLogger().debug("line=" + line + " - CALLED!");
373 // "line" must not be null or empty
376 throw new NullPointerException("line is null");
377 } else if (line.isEmpty()) {
379 throw new IllegalArgumentException("line is empty, maybe isEndOfFile() was not called?");
380 } else if (!line.endsWith(";")) {
382 throw new CorruptedDatabaseFileException(this.fileName, "No semicolon at end of line");
383 } else if (!line.contains("key=")) {
385 throw new CorruptedDatabaseFileException(this.fileName, "No \"key=bla\" found.");
386 } else if (!line.contains("value=")) {
388 throw new CorruptedDatabaseFileException(this.fileName, "No \"value=bla\" found.");
391 Pattern pattern = Pattern.compile("(key=([a-z0-9_]{1,}),value=\"([^\"]*)\";){1,}");
392 Matcher matcher = pattern.matcher(line);
395 this.getLogger().debug("matches=" + matcher.matches());
398 if (!matcher.matches()) {
399 // Corrupted file found
400 throw new CorruptedDatabaseFileException(this.fileName, "line " + line + " doesn't match regular expression.");
404 Map<String, String> map = new HashMap<>(line.length() / 40);
406 pattern = Pattern.compile("(key=([a-z0-9_]{1,}),value=\"([^\"]*)\";)");
407 matcher = pattern.matcher(line);
413 while (matcher.find(init)) {
415 String match = matcher.group(1);
416 String key = matcher.group(2);
417 String value = matcher.group(3);
419 // key must noch be empty
420 assert((key != null) && (!key.isEmpty())) : "key=" + key + " is not valid";
423 int start = matcher.start();
424 int end = matcher.end();
427 this.getLogger().debug("init=" + init + ",start=" + start + ",end=" + end + ",match=" + match + ",key=" + key + ",value=" + value);
429 // Add key/value to map
437 this.getLogger().trace("map()=" + map.size() + " - EXIT!");
439 // Return finished map
444 public final Long getTotalRows () throws IOException {
446 this.getLogger().trace("CALLED!");
454 // Walk through all rows
455 while (!this.isEndOfFile()) {
457 String line = this.readLine();
464 this.getLogger().trace("count=" + count + " - EXIT!");