1
22 package org.crosswire.jsword.book.sword;
23
24 import java.io.File;
25 import java.io.IOException;
26 import java.io.ObjectInputStream;
27 import java.io.RandomAccessFile;
28 import java.io.UnsupportedEncodingException;
29 import java.net.URI;
30 import java.text.DecimalFormat;
31 import java.text.MessageFormat;
32 import java.text.ParseException;
33 import java.util.Calendar;
34 import java.util.Date;
35 import java.util.GregorianCalendar;
36 import java.util.Locale;
37 import java.util.regex.Matcher;
38 import java.util.regex.Pattern;
39
40 import org.crosswire.common.activate.Activator;
41 import org.crosswire.common.activate.Lock;
42 import org.crosswire.common.icu.DateFormatter;
43 import org.crosswire.common.util.FileUtil;
44 import org.crosswire.common.util.Logger;
45 import org.crosswire.common.util.Reporter;
46 import org.crosswire.common.util.StringUtil;
47 import org.crosswire.jsword.JSMsg;
48 import org.crosswire.jsword.book.BookCategory;
49 import org.crosswire.jsword.book.BookException;
50 import org.crosswire.jsword.book.FeatureType;
51 import org.crosswire.jsword.passage.DefaultLeafKeyList;
52 import org.crosswire.jsword.passage.Key;
53
54
62 public class RawLDBackend extends AbstractKeyBackend {
63
70 public RawLDBackend(SwordBookMetaData sbmd, int datasize) {
71 super(sbmd);
72 this.size = -1;
73 this.datasize = datasize;
74 this.entrysize = OFFSETSIZE + datasize;
75 }
76
77
84 @Override
85 public String getRawText(Key key) throws BookException {
86 String result = getRawText(key.getName());
87 return result;
88 }
89
90 public String getRawText(String key) throws BookException {
91 if (!checkActive()) {
92 return "";
93 }
94
95 try {
96 int pos = search(key);
97 if (pos >= 0) {
98 DataEntry entry = getEntry(key, pos);
99 if (entry.isLinkEntry()) {
100 return getRawText(entry.getLinkTarget());
101 }
102 return getRawText(entry);
103 }
104 throw new BookException(JSMsg.gettext("Key not found {0}", key));
106 } catch (IOException ex) {
107 throw new BookException(JSMsg.gettext("Error reading {0}", key), ex);
110 }
111 }
112
113 protected String getRawText(DataEntry entry) {
114 String cipherKeyString = (String) getBookMetaData().getProperty(ConfigEntryType.CIPHER_KEY);
115 try {
116 return entry.getRawText((cipherKeyString != null) ? cipherKeyString.getBytes(getBookMetaData().getBookCharset()) : null);
117 } catch (UnsupportedEncodingException e) {
118 return entry.getRawText(cipherKeyString.getBytes());
119 }
120 }
121
122
127 public int getCardinality() {
128 if (!checkActive()) {
129 return 0;
130 }
131
132 if (size == -1) {
133 try {
134 size = (int) (idxRaf.length() / entrysize);
135 } catch (IOException e) {
136 size = 0;
137 }
138 }
139 return size;
140 }
141
142
147 public Key get(int index) {
148 if (checkActive()) {
149 try {
150 if (index < getCardinality()) {
151 DataEntry entry = getEntry(getBookMetaData().getInitials(), index);
152 String keytitle = internal2external(entry.getKey());
153 return new DefaultLeafKeyList(keytitle);
154 }
155 } catch (IOException e) {
156 }
158 }
159 throw new ArrayIndexOutOfBoundsException(index);
160 }
161
162
169 public int indexOf(Key that) {
170 try {
171 return search(that.getName());
172 } catch (IOException e) {
173 return -getCardinality() - 1;
174 }
175 }
176
177
184 public void activate(Lock lock) {
185 active = false;
186 size = -1;
187 idxFile = null;
188 datFile = null;
189 idxRaf = null;
190 datRaf = null;
191
192 URI path = null;
193 try {
194 path = getExpandedDataPath();
195 } catch (BookException e) {
196 Reporter.informUser(this, e);
197 return;
198 }
199
200 try {
201 idxFile = new File(path.getPath() + SwordConstants.EXTENSION_INDEX);
202 datFile = new File(path.getPath() + SwordConstants.EXTENSION_DATA);
203
204 if (!idxFile.canRead()) {
205 Reporter.informUser(this, new BookException(JSMsg.gettext("Error reading {0}", idxFile.getAbsolutePath())));
208 return;
209 }
210
211 if (!datFile.canRead()) {
212 Reporter.informUser(this, new BookException(JSMsg.gettext("Error reading {0}", datFile.getAbsolutePath())));
215 return;
216 }
217
218 idxRaf = new RandomAccessFile(idxFile, FileUtil.MODE_READ);
220 datRaf = new RandomAccessFile(datFile, FileUtil.MODE_READ);
221 } catch (IOException ex) {
222 log.error("failed to open files", ex);
223 idxRaf = null;
224 datRaf = null;
225 return;
226 }
227
228 active = true;
229 }
230
231
238 public void deactivate(Lock lock) {
239 size = -1;
240 try {
241 if (idxRaf != null) {
242 idxRaf.close();
243 }
244 if (datRaf != null) {
245 datRaf.close();
246 }
247 } catch (IOException ex) {
248 log.error("failed to close files", ex);
249 } finally {
250 idxRaf = null;
251 datRaf = null;
252 }
253
254 active = false;
255 }
256
257
260 protected boolean checkActive() {
261 if (!isActive()) {
262 Activator.activate(this);
263 }
264 return isActive();
265 }
266
267
270 protected boolean isActive() {
271 return active;
272 }
273
274
281 private DataIndex getIndex(long entry) throws IOException {
282 byte[] buffer = SwordUtil.readRAF(idxRaf, entry * entrysize, entrysize);
284 int entryOffset = SwordUtil.decodeLittleEndian32(buffer, 0);
285 int entrySize = -1;
286 switch (datasize) {
287 case 2:
288 entrySize = SwordUtil.decodeLittleEndian16(buffer, 4);
289 break;
290 case 4:
291 entrySize = SwordUtil.decodeLittleEndian32(buffer, 4);
292 break;
293 default:
294 assert false : datasize;
295 }
296 return new DataIndex(entryOffset, entrySize);
297 }
298
299
307 private DataEntry getEntry(String reply, int index) throws IOException {
308 DataIndex dataIndex = getIndex(index);
309 byte[] data = SwordUtil.readRAF(datRaf, dataIndex.getOffset(), dataIndex.getSize());
311
312 return new DataEntry(reply, data, getBookMetaData().getBookCharset());
313 }
314
315
323 private int search(String key) throws IOException {
324 if (!checkActive()) {
325 return -1;
326 }
327
328 String target = external2internal(key);
329
330 int total = getCardinality();
332 int low = 0;
338 int high = total;
339 int match = -1;
340
341 while (high - low > 1) {
342 int mid = (low + high) >>> 1;
344
345 int cmp = normalizeForSearch(getEntry(key, mid).getKey()).compareTo(target);
347 if (cmp < 0) {
348 low = mid;
349 } else if (cmp > 0) {
350 high = mid;
351 } else {
352 match = mid;
353 break;
354 }
355 }
356
357 if (match >= 0) {
359 return match;
360 }
361
362 if (normalizeForSearch(getEntry(key, 0).getKey()).compareTo(target) == 0) {
365 return 0;
366 }
367
368 return -(high + 1);
369 }
370
371
378 private String external2internal(String externalKey) {
379 SwordBookMetaData bmd = getBookMetaData();
380 String keytitle = externalKey;
381 if (BookCategory.DAILY_DEVOTIONS.equals(bmd.getBookCategory())) {
382 Calendar greg = new GregorianCalendar();
383 DateFormatter nameDF = DateFormatter.getDateInstance();
384 nameDF.setLenient(true);
385 try {
386 Date date = nameDF.parse(keytitle);
387 greg.setTime(date);
388 Object[] objs = {
389 Integer.valueOf(1 + greg.get(Calendar.MONTH)), Integer.valueOf(greg.get(Calendar.DATE))
390 };
391 return DATE_KEY_FORMAT.format(objs);
392 } catch (ParseException e) {
393 assert false : e;
394 }
395 } else if (bmd.hasFeature(FeatureType.GREEK_DEFINITIONS) || bmd.hasFeature(FeatureType.HEBREW_DEFINITIONS)) {
396 Matcher m = STRONGS_PATTERN.matcher(keytitle);
398 if (!m.matches()) {
399 return keytitle.toUpperCase(Locale.US);
400 }
401
402 int pos = keytitle.length() - 1;
404 char lastLetter = keytitle.charAt(pos);
405 boolean hasTrailingLetter = Character.isLetter(lastLetter);
406 if (hasTrailingLetter) {
407 keytitle = keytitle.substring(0, pos);
408 pos--;
410 if (pos > 0 && keytitle.charAt(pos) == '!') {
411 keytitle = keytitle.substring(0, pos);
412 }
413 }
414
415 char type = keytitle.charAt(0);
417
418 int strongsNumber = Integer.parseInt(keytitle.substring(1));
420 if (bmd.hasFeature(FeatureType.GREEK_DEFINITIONS) && bmd.hasFeature(FeatureType.HEBREW_DEFINITIONS)) {
421 StringBuilder buf = new StringBuilder();
424 buf.append(Character.toUpperCase(type));
425 buf.append(ZERO_4PAD.format(strongsNumber));
426
427 if (hasTrailingLetter && "naslex".equalsIgnoreCase(bmd.getInitials()))
430 {
431 buf.append(Character.toUpperCase(lastLetter));
432 }
433 return buf.toString();
434 }
435
436 return ZERO_5PAD.format(strongsNumber);
437 } else {
438 return keytitle.toUpperCase(Locale.US);
439 }
440
441 return keytitle;
442 }
443
444 private String internal2external(String internalKey) {
445 SwordBookMetaData bmd = getBookMetaData();
446 String keytitle = internalKey;
447 if (BookCategory.DAILY_DEVOTIONS.equals(bmd.getBookCategory()) && keytitle.length() >= 3) {
448 Calendar greg = new GregorianCalendar();
449 DateFormatter nameDF = DateFormatter.getDateInstance();
450 String[] spec = StringUtil.splitAll(keytitle, '.');
451 greg.set(Calendar.MONTH, Integer.parseInt(spec[0]) - 1);
452 greg.set(Calendar.DATE, Integer.parseInt(spec[1]));
453 keytitle = nameDF.format(greg.getTime());
454 }
455 return keytitle;
456 }
457
458 private String normalizeForSearch(String internalKey) {
459 SwordBookMetaData bmd = getBookMetaData();
460 String keytitle = internalKey;
461 if (!BookCategory.DAILY_DEVOTIONS.equals(bmd.getBookCategory())) {
462 return keytitle.toUpperCase(Locale.US);
463 }
464
465 return keytitle;
466 }
467
468
475 private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException {
476 active = false;
477 size = -1;
478 idxFile = null;
479 datFile = null;
480 idxRaf = null;
481 datRaf = null;
482 is.defaultReadObject();
483 }
484
485
488 private static final int OFFSETSIZE = 4;
489
490
493 private transient boolean active;
494
495
498 private int datasize;
499
500
503 private int entrysize;
504
505
508 private transient int size;
509
510
513 private transient File idxFile;
514
515
518 private transient RandomAccessFile idxRaf;
519
520
523 private transient File datFile;
524
525
528 private transient RandomAccessFile datRaf;
529
530
533 private static final MessageFormat DATE_KEY_FORMAT = new MessageFormat("{0,number,00}.{1,number,00}");
534
535
540 private static final Pattern STRONGS_PATTERN = Pattern.compile("^([GH])(\\d+)((!)?([a-z])?)$");
541
542
545 private static final DecimalFormat ZERO_5PAD = new DecimalFormat("00000");
546
547 private static final DecimalFormat ZERO_4PAD = new DecimalFormat("0000");
548
549
552 private static final long serialVersionUID = 818089833394450383L;
553
554
557 private static final Logger log = Logger.getLogger(RawLDBackend.class);
558 }
559