1   /**
2    * Distribution License:
3    * JSword is free software; you can redistribute it and/or modify it under
4    * the terms of the GNU Lesser General Public License, version 2.1 as published by
5    * the Free Software Foundation. This program is distributed in the hope
6    * that it will be useful, but WITHOUT ANY WARRANTY; without even the
7    * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8    * See the GNU Lesser General Public License for more details.
9    *
10   * The License is available on the internet at:
11   *       http://www.gnu.org/copyleft/lgpl.html
12   * or by writing to:
13   *      Free Software Foundation, Inc.
14   *      59 Temple Place - Suite 330
15   *      Boston, MA 02111-1307, USA
16   *
17   * Copyright: 2009-2012
18   *     The copyright to this program is held by it's authors.
19   *
20   * ID: $Id$
21   */
22  package org.crosswire.jsword.book.sword;
23  
24  import java.io.BufferedInputStream;
25  import java.io.BufferedOutputStream;
26  import java.io.File;
27  import java.io.FileInputStream;
28  import java.io.FileNotFoundException;
29  import java.io.FileOutputStream;
30  import java.io.IOException;
31  import java.io.RandomAccessFile;
32  
33  import org.crosswire.common.util.Logger;
34  import org.crosswire.jsword.book.BookException;
35  import org.crosswire.jsword.passage.Key;
36  import org.crosswire.jsword.passage.KeyUtil;
37  import org.crosswire.jsword.passage.Verse;
38  import org.crosswire.jsword.versification.Versification;
39  import org.crosswire.jsword.versification.Testament;
40  import org.crosswire.jsword.versification.system.Versifications;
41  
42  /**
43   * A Raw File format that allows for each verse to have it's own storage.
44   * The basic structure of the index is as follows:
45   * <ul>
46   * <li><strong>incfile</strong> --
47   *      Is initialized with 1 and is incremented once for each non-linked verse
48   *      that is actually stored in the Book.</li>
49   * <li><strong>idx</strong> --
50   *      There is one index file for each testament having verses, named nt and ot.
51   *      These index files contain offsets into the corresponding data file.
52   *      The idx files are indexed by the ordinal value of the verse within the Testament
53   *      for the Book's versification.</li>
54   * <li><strong>dat</strong> --
55   *      There is a data file for each testament having verses, named nt.vss and ot.vss.
56   *      These data files do not contain the verses but rather the file names that
57   *      contain the verse text.</li>
58   * <li><strong>verse</strong> --
59   *      For each stored verse there is a file containing the verse text.
60   *      The filename is a zero padded number corresponding to the current increment
61   *      from incfile, when it was created. It is this 7 character name that is stored
62   *      in a dat file.</li>
63   * </ul>
64   * 
65   * @see gnu.lgpl.License for license details.<br>
66   *      The copyright to this program is held by it's authors.
67   * @author mbergmann
68   * @author DM Smith [dmsmith555 at yahoo dot com]
69   */
70  public class RawFileBackend extends RawBackend {
71  
72      public RawFileBackend(SwordBookMetaData sbmd, int datasize) {
73          super(sbmd, datasize);
74  
75          initIncFile();
76          try {
77              incfileValue = readIncfile();
78          } catch (IOException e) {
79              log.error("Error on reading incfile!");
80          }
81      }
82  
83      /* (non-Javadoc)
84       * @see org.crosswire.jsword.book.sword.RawBackend#getRawText(org.crosswire.jsword.passage.Key)
85       */
86      @Override
87      public String getRawText(Key key) throws BookException {
88          return super.getRawText(key);
89      }
90  
91      /* (non-Javadoc)
92       * @see org.crosswire.jsword.book.sword.RawBackend#getEntry(java.lang.String, org.crosswire.jsword.versification.Testament, long)
93       */
94      @Override
95      protected String getEntry(String name, Testament testament, long index) throws IOException {
96          RandomAccessFile idxRaf = otIdxRaf;
97          RandomAccessFile txtRaf = otTxtRaf;
98          if (testament == Testament.NEW) {
99              idxRaf = ntIdxRaf;
100             txtRaf = ntTxtRaf;
101         }
102 
103         DataIndex dataIndex = getIndex(idxRaf, index);
104         int size = dataIndex.getSize();
105         if (size == 0) {
106             return "";
107         }
108 
109         if (size < 0) {
110             log.error("In " + getBookMetaData().getInitials() + ": Verse " + name + " has a bad index size of " + size);
111             return "";
112         }
113 
114         try {
115             File dataFile = getDataTextFile(txtRaf, dataIndex);
116             byte[] textBytes = readTextDataFile(dataFile);
117             decipher(textBytes);
118             return SwordUtil.decode(name, textBytes, getBookMetaData().getBookCharset());
119         } catch (BookException e) {
120             throw new IOException(e.getMessage());
121         }
122     }
123 
124     /* (non-Javadoc)
125      * @see org.crosswire.jsword.book.sword.RawBackend#setRawText(org.crosswire.jsword.passage.Key, java.lang.String)
126      */
127     @Override
128     public void setRawText(Key key, String text) throws BookException, IOException {
129         checkActive();
130 
131         Verse verse = KeyUtil.getVerse(key);
132         String v11nName = getBookMetaData().getProperty(ConfigEntryType.VERSIFICATION).toString();
133         Versification v11n = Versifications.instance().getVersification(v11nName);
134         int index = v11n.getOrdinal(verse);
135         Testament testament = v11n.getTestament(index);
136         index = v11n.getTestamentOrdinal(index);
137         RandomAccessFile idxRaf = otIdxRaf;
138         RandomAccessFile txtRaf = otTxtRaf;
139         File txtFile = otTxtFile;
140         if (testament == Testament.NEW) {
141             idxRaf = ntIdxRaf;
142             txtRaf = ntTxtRaf;
143             txtFile = ntTxtFile;
144         }
145 
146         DataIndex dataIndex = getIndex(idxRaf, index);
147         File dataFile;
148         if (dataIndex.getSize() == 0) {
149             dataFile = createDataTextFile(incfileValue);
150             updateIndexFile(idxRaf, index, txtRaf.length());
151             updateDataFile(incfileValue, txtFile);
152             checkAndIncrementIncfile(incfileValue);
153         } else {
154             dataFile = getDataTextFile(txtRaf, dataIndex);
155         }
156 
157         byte[] textData = text.getBytes("UTF-8");
158         encipher(textData);
159         writeTextDataFile(dataFile, textData);
160     }
161 
162     @Override
163     public void setAliasKey(Key alias, Key source) throws IOException {
164         Verse aliasVerse = KeyUtil.getVerse(alias);
165         Verse sourceVerse = KeyUtil.getVerse(source);
166         String v11nName = getBookMetaData().getProperty(ConfigEntryType.VERSIFICATION).toString();
167         Versification v11n = Versifications.instance().getVersification(v11nName);
168         int aliasIndex = v11n.getOrdinal(aliasVerse);
169         Testament testament = v11n.getTestament(aliasIndex);
170         aliasIndex = v11n.getTestamentOrdinal(aliasIndex);
171         RandomAccessFile idxRaf = otIdxRaf;
172         if (testament == Testament.NEW) {
173             idxRaf = ntIdxRaf;
174         }
175 
176         int sourceOIndex = v11n.getOrdinal(sourceVerse);
177         sourceOIndex = v11n.getTestamentOrdinal(sourceOIndex);
178         DataIndex dataIndex = getIndex(idxRaf, sourceOIndex);
179 
180         // Only the index is updated to point to the same place as what is linked.
181         updateIndexFile(idxRaf, aliasIndex, dataIndex.getOffset());
182     }
183 
184     private void initIncFile() {
185         try {
186             File tempIncfile = new File(getExpandedDataPath().getPath() + File.separator + INCFILE);
187             if (tempIncfile.exists()) {
188                 this.incfile = tempIncfile;
189             }
190         } catch (BookException e) {
191             log.error("Error on checking incfile: " + e.getMessage());
192         }
193     }
194 
195     private File createDataTextFile(int index) throws BookException, IOException {
196         String dataPath = getExpandedDataPath().getPath();
197         dataPath += File.separator + String.format("%07d", Integer.valueOf(index));
198         File dataFile = new File(dataPath);
199         if (!dataFile.exists() && !dataFile.createNewFile()) {
200             throw new IOException("Could not create data file.");
201         }
202         return dataFile;
203     }
204 
205     /**
206      * Gets the Filename for the File having the verse text.
207      * 
208      * @param txtRaf The random access file containing the file names for the verse storage.
209      * @param dataIndex The index of where to get the data
210      * @return the file having the verse text.
211      * @throws IOException
212      */
213     private String getTextFilename(RandomAccessFile txtRaf, DataIndex dataIndex) throws IOException {
214         // data size to be read from the data file (ot or nt) should be 9 bytes
215         // this will be the filename of the actual text file "\r\n"
216         byte[] data = SwordUtil.readRAF(txtRaf, dataIndex.getOffset(), dataIndex.getSize());
217         decipher(data);
218         if (data.length == 7) {
219             return new String(data, 0, 7);
220         }
221         log.error("Read data is not of appropriate size of 9 bytes!");
222         throw new IOException("Datalength is not 9 bytes!");
223     }
224 
225     /**
226      * Gets the File having the verse text.
227      * 
228      * @param txtRaf The random access file containing the file names for the verse storage.
229      * @param dataIndex The index of where to get the data
230      * @return the file having the verse text.
231      * @throws IOException
232      * @throws BookException
233      */
234     private File getDataTextFile(RandomAccessFile txtRaf, DataIndex dataIndex) throws IOException, BookException {
235         String dataFilename = getTextFilename(txtRaf, dataIndex);
236         String dataPath = getExpandedDataPath().getPath() + File.separator + dataFilename;
237         return new File(dataPath);
238     }
239 
240 
241     protected void updateIndexFile(RandomAccessFile idxRaf, long index, long dataFileStartPosition) throws IOException {
242         long indexFileWriteOffset = index * entrysize;
243         int dataFileLengthValue = 7; // filename is 7 bytes + 2 bytes for "\r\n"
244         byte[] startPositionData = littleEndian32BitByteArrayFromInt((int) dataFileStartPosition);
245         byte[] lengthValueData = littleEndian16BitByteArrayFromShort((short) dataFileLengthValue);
246         byte[] indexFileWriteData = new byte[6];
247 
248         indexFileWriteData[0] = startPositionData[0];
249         indexFileWriteData[1] = startPositionData[1];
250         indexFileWriteData[2] = startPositionData[2];
251         indexFileWriteData[3] = startPositionData[3];
252         indexFileWriteData[4] = lengthValueData[0];
253         indexFileWriteData[5] = lengthValueData[1];
254 
255         SwordUtil.writeRAF(idxRaf, indexFileWriteOffset, indexFileWriteData);
256     }
257 
258     protected void updateDataFile(long ordinal, File txtFile) throws IOException {
259         String fileName = String.format("%07d\r\n", Long.valueOf(ordinal));
260         BufferedOutputStream bos = null;
261         try {
262             bos = new BufferedOutputStream(new FileOutputStream(txtFile, true));
263             bos.write(fileName.getBytes(getBookMetaData().getBookCharset()));
264         } finally {
265             if (bos != null) {
266                 bos.close();
267             }
268         }
269     }
270 
271     private void checkAndIncrementIncfile(int index) throws IOException {
272         if (index >= this.incfileValue) {
273             this.incfileValue = index + 1;
274             writeIncfile(this.incfileValue);
275         }
276     }
277 
278     /*
279      * (non-Javadoc)
280      * 
281      * @see org.crosswire.jsword.book.sword.RawBackend#create()
282      */
283     @Override
284     public void create() throws IOException, BookException {
285         super.create();
286         createDataFiles();
287         createIndexFiles();
288         createIncfile();
289 
290         checkActive();
291 
292         prepopulateIndexFiles();
293         prepopulateIncfile();
294     }
295 
296     /* (non-Javadoc)
297      * @see org.crosswire.jsword.book.sword.RawBackend#isWritable()
298      */
299     @Override
300     public boolean isWritable() {
301         File incFile = this.incfile;
302 
303         if (otTxtFile.exists() && otTxtFile.canRead() && otTxtFile.canWrite() && ntTxtFile.exists() && ntTxtFile.canRead() && ntTxtFile.canWrite()
304                 && otIdxFile.exists() && otIdxFile.canRead() && otIdxFile.canWrite() && ntIdxFile.exists() && ntIdxFile.canRead()
305                 && ntIdxFile.canWrite() && incFile.exists() && incFile.canRead() && incFile.canWrite())
306         {
307             return true;
308         }
309         return false;
310     }
311 
312     private void createDataFiles() throws IOException, BookException {
313         String path = getExpandedDataPath().getPath();
314 
315         File otTextFile = new File(path + File.separator + SwordConstants.FILE_OT);
316         if (!otTextFile.exists() && !otTextFile.createNewFile()) {
317             throw new IOException("Could not create ot text file.");
318         }
319 
320         File ntTextFile = new File(path + File.separator + SwordConstants.FILE_NT);
321         if (!ntTextFile.exists() && !ntTextFile.createNewFile()) {
322             throw new IOException("Could not create nt text file.");
323         }
324     }
325 
326     private void createIndexFiles() throws IOException, BookException {
327         String path = getExpandedDataPath().getPath();
328         File otIndexFile = new File(path + File.separator + SwordConstants.FILE_OT + SwordConstants.EXTENSION_VSS);
329         if (!otIndexFile.exists() && !otIndexFile.createNewFile()) {
330             throw new IOException("Could not create ot index file.");
331         }
332 
333         File ntIndexFile = new File(path + File.separator + SwordConstants.FILE_NT + SwordConstants.EXTENSION_VSS);
334         if (!ntIndexFile.exists() && !ntIndexFile.createNewFile()) {
335             throw new IOException("Could not create nt index file.");
336         }
337     }
338 
339     private void prepopulateIndexFiles() throws IOException {
340         String v11nName = getBookMetaData().getProperty(ConfigEntryType.VERSIFICATION).toString();
341         Versification v11n = Versifications.instance().getVersification(v11nName);
342         int otCount = v11n.getCount(Testament.OLD);
343         int ntCount = v11n.getCount(Testament.NEW) + 1;
344         BufferedOutputStream otIdxBos = new BufferedOutputStream(new FileOutputStream(otIdxFile, false));
345         try {
346             for (int i = 0; i < otCount; i++) {
347                 writeInitialIndex(otIdxBos);
348             }
349         } finally {
350             otIdxBos.close();
351         }
352 
353         BufferedOutputStream ntIdxBos = new BufferedOutputStream(new FileOutputStream(ntIdxFile, false));
354         try {
355             for (int i = 0; i < ntCount; i++) {
356                 writeInitialIndex(ntIdxBos);
357             }
358         } finally {
359             ntIdxBos.close();
360         }
361     }
362 
363     private void createIncfile() throws IOException, BookException {
364         File tempIncfile = new File(getExpandedDataPath().getPath() + File.separator + INCFILE);
365         if (!tempIncfile.exists() && !tempIncfile.createNewFile()) {
366             throw new IOException("Could not create incfile file.");
367         }
368         this.incfile = tempIncfile;
369     }
370 
371     private void prepopulateIncfile() throws IOException {
372         writeIncfile(1);
373     }
374 
375     private void writeIncfile(int value) throws IOException {
376         FileOutputStream fos = null;
377         try {
378             fos = new FileOutputStream(this.incfile, false);
379             fos.write(littleEndian32BitByteArrayFromInt(value));
380         } catch (FileNotFoundException e) {
381             log.error("Error on writing to incfile, file should exist already!");
382             log.error(e.getMessage());
383         } finally {
384             if (fos != null) {
385                 fos.close();
386             }
387         }
388     }
389 
390     private int readIncfile() throws IOException {
391         int ret = -1;
392         if (this.incfile != null) {
393             FileInputStream fis = null;
394             try {
395                 fis = new FileInputStream(this.incfile);
396                 byte[] buffer = new byte[4];
397                 if (fis.read(buffer) != 4) {
398                     log.error("Read data is not of appropriate size of 4 bytes!");
399                     throw new IOException("Incfile is not 4 bytes long");
400                 }
401                 ret = SwordUtil.decodeLittleEndian32(buffer, 0);
402             } catch (FileNotFoundException e) {
403                 log.error("Error on writing to incfile, file should exist already!");
404                 log.error(e.getMessage());
405             } finally {
406                 if (fis != null) {
407                     fis.close();
408                 }
409             }
410         }
411 
412         return ret;
413     }
414 
415     private void writeInitialIndex(BufferedOutputStream outStream) throws IOException {
416         outStream.write(littleEndian32BitByteArrayFromInt(0)); // offset
417         outStream.write(littleEndian16BitByteArrayFromShort((short) 0)); // length
418     }
419 
420     private byte[] readTextDataFile(File dataFile) throws IOException {
421         BufferedInputStream inStream = null;
422         try {
423             int len = (int) dataFile.length();
424             byte[] textData = new byte[len];
425             inStream = new BufferedInputStream(new FileInputStream(dataFile));
426             if (inStream.read(textData) != len) {
427                 log.error("Read data is not of appropriate size of " + len + " bytes!");
428                 throw new IOException("data is not " + len + " bytes long");
429             }
430             return textData;
431         } catch (FileNotFoundException ex) {
432             log.error(ex.getMessage());
433             throw new IOException("Could not read text data file, file not found: " + dataFile.getName());
434         } finally {
435             if (inStream != null) {
436                 inStream.close();
437             }
438         }
439     }
440 
441     private void writeTextDataFile(File dataFile, byte[] textData) throws IOException {
442         BufferedOutputStream bos = null;
443         try {
444             bos = new BufferedOutputStream(new FileOutputStream(dataFile, false));
445             bos.write(textData);
446         } finally {
447             if (bos != null) {
448                 bos.close();
449             }
450         }
451     }
452 
453     private byte[] littleEndian32BitByteArrayFromInt(int val) {
454         byte[] buffer = new byte[4];
455         SwordUtil.encodeLittleEndian32(val, buffer, 0);
456         return buffer;
457     }
458 
459     private byte[] littleEndian16BitByteArrayFromShort(short val) {
460         byte[] buffer = new byte[2];
461         SwordUtil.encodeLittleEndian16(val, buffer, 0);
462         return buffer;
463     }
464 
465     private static final String INCFILE = "incfile";
466 
467     private File incfile;
468     private int incfileValue;
469 
470     private static final Logger log = Logger.getLogger(RawFileBackend.class);
471 }
472