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