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, 2005 - 2016
18   *
19   */
20  package org.crosswire.jsword.book.sword;
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.RandomAccessFile;
25  import java.net.URI;
26  
27  import org.crosswire.common.activate.Activatable;
28  import org.crosswire.common.activate.Activator;
29  import org.crosswire.common.activate.Lock;
30  import org.crosswire.common.util.FileUtil;
31  import org.crosswire.common.util.NetUtil;
32  import org.crosswire.common.util.Reporter;
33  import org.crosswire.jsword.JSMsg;
34  import org.crosswire.jsword.JSOtherMsg;
35  import org.crosswire.jsword.book.BookException;
36  import org.crosswire.jsword.passage.DefaultKeyList;
37  import org.crosswire.jsword.passage.Key;
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  
41  /**
42   * TreeKeyIndex reads Sword index files that are path based. Paths are of the
43   * form /a/b/c, and can be of any depth. The ultimate output of a TreeKeyIndex
44   * is the offset and length of a chunk of data in another file that can be read.
45   * 
46   * @see gnu.lgpl.License The GNU Lesser General Public License for details.
47   * @author DM Smith
48   */
49  public class TreeKeyIndex implements Activatable {
50      /**
51       * Simple ctor
52       * 
53       * @param sbmd 
54       */
55      public TreeKeyIndex(SwordBookMetaData sbmd) {
56          bmd = sbmd;
57      }
58  
59      /**
60       * @return the root TreeNode for the module.
61       * @throws IOException
62       */
63      public TreeNode getRoot() throws IOException {
64          return getTreeNode(getOffset(0));
65      }
66  
67      /**
68       * Get the parent of the TreeNode.
69       * 
70       * @param node
71       *            the node being worked upon
72       * @return the parent node
73       * @throws IOException
74       */
75      public TreeNode getParent(TreeNode node) throws IOException {
76          return getTreeNode(getOffset(node.getParent()));
77      }
78  
79      /**
80       * Get the first child of the TreeNode.
81       * 
82       * @param node
83       *            the node being worked upon
84       * @return the first child node
85       * @throws IOException
86       */
87      public TreeNode getFirstChild(TreeNode node) throws IOException {
88          return getTreeNode(getOffset(node.getFirstChild()));
89      }
90  
91      /**
92       * Get the next sibling of the TreeNode.
93       * 
94       * @param node
95       *            the node being worked upon
96       * @return the next sibling node
97       * @throws IOException
98       */
99      public TreeNode getNextSibling(TreeNode node) throws IOException {
100         return getTreeNode(getOffset(node.getNextSibling()));
101     }
102 
103     /**
104      * The idx file contains offsets into the dat file.
105      * 
106      * @param index
107      *            the record id
108      * @return an offset into the dat file
109      * @throws IOException
110      */
111     private int getOffset(int index) throws IOException {
112         if (index == -1) {
113             return -1;
114         }
115 
116         checkActive();
117         byte[] buffer = SwordUtil.readRAF(idxRaf, index, 4);
118         return SwordUtil.decodeLittleEndian32(buffer, 0);
119     }
120 
121     /**
122      * Given an offset get the TreeNode from the dat file.
123      * 
124      * @param offset
125      *            start of a TreeNode record in the dat file.
126      * @return the TreeNode
127      * @throws IOException
128      */
129     private TreeNode getTreeNode(int offset) throws IOException {
130         TreeNode node = new TreeNode(offset);
131 
132         if (offset == -1) {
133             return node;
134         }
135 
136         checkActive();
137         byte[] buffer = SwordUtil.readRAF(datRaf, offset, 12);
138         node.setParent(SwordUtil.decodeLittleEndian32(buffer, 0));
139         node.setNextSibling(SwordUtil.decodeLittleEndian32(buffer, 4));
140         node.setFirstChild(SwordUtil.decodeLittleEndian32(buffer, 8));
141 
142         buffer = SwordUtil.readUntilRAF(datRaf, (byte) 0);
143         int size = buffer.length;
144         if (buffer[size - 1] == 0) {
145             size--;
146         }
147 
148         Key key = new DefaultKeyList(null, bmd.getName());
149         // Some of the keys have extraneous whitespace, so remove it.
150         node.setName(SwordUtil.decode(key.getName(), buffer, size, bmd.getBookCharset()).trim());
151 
152         buffer = SwordUtil.readNextRAF(datRaf, 2);
153         int userDataSize = SwordUtil.decodeLittleEndian16(buffer, 0);
154         if (userDataSize > 0) {
155             node.setUserData(SwordUtil.readNextRAF(datRaf, userDataSize));
156         }
157 
158         return node;
159     }
160 
161     /* (non-Javadoc)
162      * @see org.crosswire.common.activate.Activatable#activate(org.crosswire.common.activate.Lock)
163      */
164     public final void activate(Lock lock) {
165         String path = null;
166         try {
167             path = getExpandedDataPath();
168         } catch (BookException e) {
169             Reporter.informUser(this, e);
170             return;
171         }
172 
173         idxFile = new File(path + EXTENSION_INDEX);
174         datFile = new File(path + EXTENSION_DATA);
175 
176         if (!idxFile.canRead()) {
177             // TRANSLATOR: Common error condition: The file could not be read. There can be many reasons.
178             // {0} is a placeholder for the file.
179             Reporter.informUser(this, new BookException(JSMsg.gettext("Error reading {0}", idxFile.getAbsolutePath())));
180             return;
181         }
182 
183         if (!datFile.canRead()) {
184             // TRANSLATOR: Common error condition: The file could not be read. There can be many reasons.
185             // {0} is a placeholder for the file.
186             Reporter.informUser(this, new BookException(JSMsg.gettext("Error reading {0}", datFile.getAbsolutePath())));
187             return;
188         }
189 
190         try {
191             idxRaf = new RandomAccessFile(idxFile, FileUtil.MODE_READ);
192             datRaf = new RandomAccessFile(datFile, FileUtil.MODE_READ);
193         } catch (IOException ex) {
194             log.error("failed to open files", ex);
195             idxRaf = null;
196             datRaf = null;
197         }
198         active = true;
199     }
200 
201     /* (non-Javadoc)
202      * @see org.crosswire.common.activate.Activatable#deactivate(org.crosswire.common.activate.Lock)
203      */
204     public final void deactivate(Lock lock) {
205         try {
206             if (idxRaf != null) {
207                 idxRaf.close();
208             }
209             if (datRaf != null) {
210                 datRaf.close();
211             }
212         } catch (IOException ex) {
213             log.error("failed to close nt files", ex);
214         } finally {
215             idxRaf = null;
216             datRaf = null;
217         }
218         active = false;
219     }
220 
221     /**
222      * Helper method so we can quickly activate ourselves on access
223      */
224     protected final void checkActive() {
225         if (!active) {
226             Activator.activate(this);
227         }
228     }
229 
230     private String getExpandedDataPath() throws BookException {
231         URI loc = NetUtil.lengthenURI(bmd.getLibrary(), bmd.getProperty(SwordBookMetaData.KEY_DATA_PATH));
232 
233         if (loc == null) {
234             // FIXME(DMS): missing parameter
235             throw new BookException(JSOtherMsg.lookupText("Missing data files for old and new testaments in {0}."));
236         }
237 
238         return new File(loc.getPath()).getAbsolutePath();
239     }
240 
241     private static final String EXTENSION_INDEX = ".idx";
242     private static final String EXTENSION_DATA = ".dat";
243 
244     private SwordBookMetaData bmd;
245     private File idxFile;
246     private File datFile;
247     private RandomAccessFile idxRaf;
248     private RandomAccessFile datRaf;
249     private boolean active;
250 
251     /**
252      * The log stream
253      */
254     private static final Logger log = LoggerFactory.getLogger(TreeKeyIndex.class);
255 }
256