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 or later
5    * as published by the Free Software Foundation. This program is distributed
6    * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
7    * the 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   * © CrossWire Bible Society, 2009 - 2016
18   *
19   */
20  package org.crosswire.jsword.book.sword;
21  
22  import java.io.BufferedInputStream;
23  import java.io.BufferedOutputStream;
24  import java.io.File;
25  import java.io.FileInputStream;
26  import java.io.FileNotFoundException;
27  import java.io.FileOutputStream;
28  import java.io.IOException;
29  import java.io.RandomAccessFile;
30  
31  import org.crosswire.jsword.book.BookException;
32  import org.crosswire.jsword.book.BookMetaData;
33  import org.crosswire.jsword.book.sword.state.OpenFileStateManager;
34  import org.crosswire.jsword.book.sword.state.RawBackendState;
35  import org.crosswire.jsword.book.sword.state.RawFileBackendState;
36  import org.crosswire.jsword.passage.Key;
37  import org.crosswire.jsword.passage.KeyUtil;
38  import org.crosswire.jsword.passage.Verse;
39  import org.crosswire.jsword.versification.Testament;
40  import org.crosswire.jsword.versification.Versification;
41  import org.crosswire.jsword.versification.system.Versifications;
42  import org.slf4j.Logger;
43  import org.slf4j.LoggerFactory;
44  
45  /**
46   * A Raw File format that allows for each verse to have it's own storage. The
47   * basic structure of the index is as follows:
48   * <ul>
49   * <li><strong>incfile</strong> -- Is initialized with 1 and is incremented once
50   * for each non-linked verse that is actually stored in the Book.</li>
51   * <li><strong>idx</strong> -- There is one index file for each testament having
52   * verses, named nt and ot. These index files contain offsets into the
53   * corresponding data file. The idx files are indexed by the ordinal value of
54   * the verse within the Testament for the Book's versification.</li>
55   * <li><strong>dat</strong> -- There is a data file for each testament having
56   * verses, named nt.vss and ot.vss. These data files do not contain the verses
57   * but rather the file names that contain the verse text.</li>
58   * <li><strong>verse</strong> -- For each stored verse there is a file
59   * containing the verse text. The filename is a zero padded number corresponding
60   * to the current increment from incfile, when it was created. It is this 7
61   * character name that is stored in a dat file.</li>
62   * </ul>
63   * 
64   * @see gnu.lgpl.License The GNU Lesser General Public License for details.
65   * @author mbergmann
66   * @author DM Smith
67   */
68  public class RawFileBackend extends RawBackend<RawFileBackendState> {
69  
70      public RawFileBackend(SwordBookMetaData sbmd, int datasize) {
71          super(sbmd, datasize);
72      }
73  
74      @Override
75      public RawFileBackendState initState() throws BookException {
76          return OpenFileStateManager.instance().getRawFileBackendState(getBookMetaData());
77      }
78  
79      /* (non-Javadoc)
80       * @see org.crosswire.jsword.book.sword.RawBackend#getEntry(java.lang.String, org.crosswire.jsword.versification.Testament, long)
81       */
82      @Override
83      protected String getEntry(RawBackendState state, String name, Testament testament, long index) throws IOException {
84          RandomAccessFile idxRaf;
85          RandomAccessFile txtRaf;
86          idxRaf = state.getIdxRaf(testament);
87          txtRaf = state.getTextRaf(testament);
88  
89          DataIndex dataIndex = getIndex(idxRaf, index);
90          int size = dataIndex.getSize();
91          if (size == 0) {
92              return "";
93          }
94  
95          if (size < 0) {
96              log.error("In {}: Verse {} has a bad index size of {}.", getBookMetaData().getInitials(), name, Integer.toString(size));
97              return "";
98          }
99  
100         try {
101             File dataFile = getDataTextFile(txtRaf, dataIndex);
102             byte[] textBytes = readTextDataFile(dataFile);
103             decipher(textBytes);
104             return SwordUtil.decode(name, textBytes, getBookMetaData().getBookCharset());
105         } catch (BookException e) {
106             throw new IOException(e.getMessage());
107         }
108     }
109 
110     /* (non-Javadoc)
111      * @see org.crosswire.jsword.book.sword.RawBackend#setRawText(org.crosswire.jsword.passage.Key, java.lang.String)
112      * 
113      */
114     public void setRawText(RawFileBackendState state, Key key, String text) throws BookException, IOException {
115 
116         String v11nName = getBookMetaData().getProperty(BookMetaData.KEY_VERSIFICATION);
117         Versification v11n = Versifications.instance().getVersification(v11nName);
118         Verse verse = KeyUtil.getVerse(key);
119         int index = verse.getOrdinal();
120         Testament testament = v11n.getTestament(index);
121         index = v11n.getTestamentOrdinal(index);
122 
123         RandomAccessFile idxRaf = state.getIdxRaf(testament);
124         RandomAccessFile txtRaf = state.getTextRaf(testament);
125         File txtFile = state.getTextFile(testament);
126 
127         DataIndex dataIndex = getIndex(idxRaf, index);
128         File dataFile;
129         if (dataIndex.getSize() == 0) {
130             dataFile = createDataTextFile(state.getIncfileValue());
131             updateIndexFile(idxRaf, index, txtRaf.length());
132             updateDataFile(state.getIncfileValue(), txtFile);
133             checkAndIncrementIncfile(state, state.getIncfileValue());
134         } else {
135             dataFile = getDataTextFile(txtRaf, dataIndex);
136         }
137 
138         byte[] textData = text.getBytes("UTF-8");
139         encipher(textData);
140         writeTextDataFile(dataFile, textData);
141     }
142 
143     public void setAliasKey(RawFileBackendState state, Key alias, Key source) throws IOException {
144         String v11nName = getBookMetaData().getProperty(BookMetaData.KEY_VERSIFICATION);
145         Versification v11n = Versifications.instance().getVersification(v11nName);
146         Verse aliasVerse = KeyUtil.getVerse(alias);
147         Verse sourceVerse = KeyUtil.getVerse(source);
148         int aliasIndex = aliasVerse.getOrdinal();
149         Testament testament = v11n.getTestament(aliasIndex);
150         aliasIndex = v11n.getTestamentOrdinal(aliasIndex);
151         RandomAccessFile idxRaf = state.getIdxRaf(testament);
152 
153         int sourceOIndex = sourceVerse.getOrdinal();
154         sourceOIndex = v11n.getTestamentOrdinal(sourceOIndex);
155         DataIndex dataIndex = getIndex(idxRaf, sourceOIndex);
156 
157         // Only the index is updated to point to the same place as what is
158         // linked.
159         updateIndexFile(idxRaf, aliasIndex, dataIndex.getOffset());
160     }
161 
162     private File createDataTextFile(int index) throws BookException, IOException {
163         String dataPath = SwordUtil.getExpandedDataPath(getBookMetaData()).getPath();
164         dataPath += File.separator + String.format("%07d", Integer.valueOf(index));
165         File dataFile = new File(dataPath);
166         if (!dataFile.exists() && !dataFile.createNewFile()) {
167             throw new IOException("Could not create data file.");
168         }
169         return dataFile;
170     }
171 
172     /**
173      * Gets the Filename for the File having the verse text.
174      * 
175      * @param txtRaf
176      *            The random access file containing the file names for the verse
177      *            storage.
178      * @param dataIndex
179      *            The index of where to get the data
180      * @return the file having the verse text.
181      * @throws IOException
182      */
183     private String getTextFilename(RandomAccessFile txtRaf, DataIndex dataIndex) throws IOException {
184         // data size to be read from the data file (ot or nt) should be 9 bytes
185         // this will be the filename of the actual text file "\r\n"
186         byte[] data = SwordUtil.readRAF(txtRaf, dataIndex.getOffset(), dataIndex.getSize());
187         decipher(data);
188         if (data.length == 7) {
189             return new String(data, 0, 7);
190         }
191         log.error("Read data is not of appropriate size of 9 bytes!");
192         throw new IOException("Datalength is not 9 bytes!");
193     }
194 
195     /**
196      * Gets the File having the verse text.
197      * 
198      * @param txtRaf
199      *            The random access file containing the file names for the verse
200      *            storage.
201      * @param dataIndex
202      *            The index of where to get the data
203      * @return the file having the verse text.
204      * @throws IOException
205      * @throws BookException
206      */
207     private File getDataTextFile(RandomAccessFile txtRaf, DataIndex dataIndex) throws IOException, BookException {
208         String dataFilename = getTextFilename(txtRaf, dataIndex);
209         String dataPath = SwordUtil.getExpandedDataPath(getBookMetaData()).getPath() + File.separator + dataFilename;
210         return new File(dataPath);
211     }
212 
213     protected void updateIndexFile(RandomAccessFile idxRaf, long index, long dataFileStartPosition) throws IOException {
214         long indexFileWriteOffset = index * entrysize;
215         int dataFileLengthValue = 7; // filename is 7 bytes + 2 bytes for "\r\n"
216         byte[] startPositionData = littleEndian32BitByteArrayFromInt((int) dataFileStartPosition);
217         byte[] lengthValueData = littleEndian16BitByteArrayFromShort((short) dataFileLengthValue);
218         byte[] indexFileWriteData = new byte[6];
219 
220         indexFileWriteData[0] = startPositionData[0];
221         indexFileWriteData[1] = startPositionData[1];
222         indexFileWriteData[2] = startPositionData[2];
223         indexFileWriteData[3] = startPositionData[3];
224         indexFileWriteData[4] = lengthValueData[0];
225         indexFileWriteData[5] = lengthValueData[1];
226 
227         SwordUtil.writeRAF(idxRaf, indexFileWriteOffset, indexFileWriteData);
228     }
229 
230     protected void updateDataFile(long ordinal, File txtFile) throws IOException {
231         String fileName = String.format("%07d\r\n", Long.valueOf(ordinal));
232         BufferedOutputStream bos = null;
233         try {
234             bos = new BufferedOutputStream(new FileOutputStream(txtFile, true));
235             bos.write(fileName.getBytes(getBookMetaData().getBookCharset()));
236         } finally {
237             if (bos != null) {
238                 bos.close();
239             }
240         }
241     }
242 
243     private void checkAndIncrementIncfile(RawFileBackendState state, int index) throws IOException {
244         if (index >= state.getIncfileValue()) {
245             int incValue = index + 1;
246 
247             // FIXME(CJB) this portion of code is unsafe for concurrency
248             // operations, but without knowing the
249             // reason for the inc file, I would guess it unlikely to be a
250             // problem
251             // this essentially destroys consistency of the state
252             state.setIncfileValue(incValue);
253             writeIncfile(state, incValue);
254         }
255     }
256 
257     /*
258      * (non-Javadoc)
259      * 
260      * @see org.crosswire.jsword.book.sword.RawBackend#create()
261      */
262     // FIXME(CJB) : DM - API change to pass in state, or is this a one-off for
263     // which we want to release resources afterwards (opted for the second
264     // option)
265     @Override
266     public void create() throws IOException, BookException {
267         super.create();
268 
269         createDataFiles();
270         createIndexFiles();
271 
272         RawFileBackendState state = null;
273         try {
274             state = initState();
275 
276             createIncfile(state);
277 
278             prepopulateIndexFiles(state);
279             prepopulateIncfile(state);
280         } finally {
281             OpenFileStateManager.instance().release(state);
282         }
283     }
284 
285     private void createDataFiles() throws IOException, BookException {
286         String path = SwordUtil.getExpandedDataPath(getBookMetaData()).getPath();
287 
288         File otTextFile = new File(path + File.separator + SwordConstants.FILE_OT);
289         if (!otTextFile.exists() && !otTextFile.createNewFile()) {
290             throw new IOException("Could not create ot text file.");
291         }
292 
293         File ntTextFile = new File(path + File.separator + SwordConstants.FILE_NT);
294         if (!ntTextFile.exists() && !ntTextFile.createNewFile()) {
295             throw new IOException("Could not create nt text file.");
296         }
297     }
298 
299     private void createIndexFiles() throws IOException, BookException {
300         String path = SwordUtil.getExpandedDataPath(getBookMetaData()).getPath();
301         File otIndexFile = new File(path + File.separator + SwordConstants.FILE_OT + SwordConstants.EXTENSION_VSS);
302         if (!otIndexFile.exists() && !otIndexFile.createNewFile()) {
303             throw new IOException("Could not create ot index file.");
304         }
305 
306         File ntIndexFile = new File(path + File.separator + SwordConstants.FILE_NT + SwordConstants.EXTENSION_VSS);
307         if (!ntIndexFile.exists() && !ntIndexFile.createNewFile()) {
308             throw new IOException("Could not create nt index file.");
309         }
310     }
311 
312     private void prepopulateIndexFiles(RawFileBackendState state) throws IOException {
313 
314         String v11nName = getBookMetaData().getProperty(BookMetaData.KEY_VERSIFICATION);
315         Versification v11n = Versifications.instance().getVersification(v11nName);
316         int otCount = v11n.getCount(Testament.OLD);
317         int ntCount = v11n.getCount(Testament.NEW) + 1;
318         BufferedOutputStream otIdxBos = new BufferedOutputStream(new FileOutputStream(state.getIdxFile(Testament.OLD), false));
319         try {
320             for (int i = 0; i < otCount; i++) {
321                 writeInitialIndex(otIdxBos);
322             }
323         } finally {
324             otIdxBos.close();
325         }
326 
327         BufferedOutputStream ntIdxBos = new BufferedOutputStream(new FileOutputStream(state.getIdxFile(Testament.NEW), false));
328         try {
329             for (int i = 0; i < ntCount; i++) {
330                 writeInitialIndex(ntIdxBos);
331             }
332         } finally {
333             ntIdxBos.close();
334         }
335     }
336 
337     private void createIncfile(RawFileBackendState state) throws IOException, BookException {
338         File tempIncfile = new File(SwordUtil.getExpandedDataPath(getBookMetaData()).getPath() + File.separator + RawFileBackendState.INCFILE);
339         if (!tempIncfile.exists() && !tempIncfile.createNewFile()) {
340             throw new IOException("Could not create incfile file.");
341         }
342         state.setIncfile(tempIncfile);
343     }
344 
345     private void prepopulateIncfile(RawFileBackendState state) throws IOException {
346         writeIncfile(state, 1);
347     }
348 
349     private void writeIncfile(RawFileBackendState state, int value) throws IOException {
350         FileOutputStream fos = null;
351         try {
352             fos = new FileOutputStream(state.getIncfile(), false);
353             fos.write(littleEndian32BitByteArrayFromInt(value));
354         } catch (FileNotFoundException e) {
355             log.error("Error on writing to incfile, file should exist already!", e);
356         } finally {
357             if (fos != null) {
358                 fos.close();
359             }
360         }
361     }
362 
363     private void writeInitialIndex(BufferedOutputStream outStream) throws IOException {
364         outStream.write(littleEndian32BitByteArrayFromInt(0)); // offset
365         outStream.write(littleEndian16BitByteArrayFromShort((short) 0)); // length
366     }
367 
368     private byte[] readTextDataFile(File dataFile) throws IOException {
369         BufferedInputStream inStream = null;
370         try {
371             int len = (int) dataFile.length();
372             byte[] textData = new byte[len];
373             inStream = new BufferedInputStream(new FileInputStream(dataFile));
374             if (inStream.read(textData) != len) {
375                 log.error("Read data is not of appropriate size of {} bytes!", Integer.toString(len));
376                 throw new IOException("data is not " + len + " bytes long");
377             }
378             return textData;
379         } catch (FileNotFoundException ex) {
380             log.error("Could not read text data file, file not found: {}", dataFile.getName(), ex);
381             throw ex;
382         } finally {
383             if (inStream != null) {
384                 inStream.close();
385             }
386         }
387     }
388 
389     private void writeTextDataFile(File dataFile, byte[] textData) throws IOException {
390         BufferedOutputStream bos = null;
391         try {
392             bos = new BufferedOutputStream(new FileOutputStream(dataFile, false));
393             bos.write(textData);
394         } finally {
395             if (bos != null) {
396                 bos.close();
397             }
398         }
399     }
400 
401     private byte[] littleEndian32BitByteArrayFromInt(int val) {
402         byte[] buffer = new byte[4];
403         SwordUtil.encodeLittleEndian32(val, buffer, 0);
404         return buffer;
405     }
406 
407     private byte[] littleEndian16BitByteArrayFromShort(short val) {
408         byte[] buffer = new byte[2];
409         SwordUtil.encodeLittleEndian16(val, buffer, 0);
410         return buffer;
411     }
412 
413     private static final Logger log = LoggerFactory.getLogger(RawFileBackend.class);
414 }
415