Coverage Report - org.crosswire.jsword.book.BookData
 
Classes in this File Line Coverage Branch Coverage Complexity
BookData
0%
0/186
0%
0/116
3.579
BookData$BookVerseContent
0%
0/1
N/A
3.579
 
 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;
 21  
 
 22  
 import java.util.ArrayList;
 23  
 import java.util.Iterator;
 24  
 import java.util.List;
 25  
 import java.util.Map;
 26  
 import java.util.TreeMap;
 27  
 
 28  
 import org.crosswire.common.diff.Diff;
 29  
 import org.crosswire.common.diff.DiffCleanup;
 30  
 import org.crosswire.common.diff.Difference;
 31  
 import org.crosswire.common.util.Language;
 32  
 import org.crosswire.common.xml.JDOMSAXEventProvider;
 33  
 import org.crosswire.common.xml.SAXEventProvider;
 34  
 import org.crosswire.jsword.passage.Key;
 35  
 import org.crosswire.jsword.passage.KeyUtil;
 36  
 import org.crosswire.jsword.passage.Passage;
 37  
 import org.crosswire.jsword.passage.RestrictionType;
 38  
 import org.crosswire.jsword.passage.Verse;
 39  
 import org.crosswire.jsword.versification.Versification;
 40  
 import org.crosswire.jsword.versification.VersificationsMapper;
 41  
 import org.crosswire.jsword.versification.system.Versifications;
 42  
 import org.jdom2.Content;
 43  
 import org.jdom2.Document;
 44  
 import org.jdom2.Element;
 45  
 import org.jdom2.Namespace;
 46  
 import org.jdom2.Text;
 47  
 
 48  
 /**
 49  
  * BookData is the assembler of the OSIS that is returned by the filters. As
 50  
  * such it puts that into an OSIS document. When several books are supplied, it
 51  
  * gets the data from each and puts it into a parallel or interlinear view.
 52  
  * Note: it is critical that all the books are able to understand the same key.
 53  
  * That does not mean that each has to have content for each key. Missing keys
 54  
  * are represented by empty cells.
 55  
  *
 56  
  * @author Joe Walker
 57  
  * @author DM Smith
 58  
  * @see gnu.lgpl.License The GNU Lesser General Public License for details.
 59  
  */
 60  0
 public class BookData implements BookProvider {
 61  
     /**
 62  
      * Create a BookData.
 63  
      * 
 64  
      * @param book the Book to which the data belongs
 65  
      * @param key the Key specifying the data
 66  
      */
 67  0
     public BookData(Book book, Key key) {
 68  0
         assert book != null;
 69  0
         assert key != null;
 70  
 
 71  0
         this.key = key;
 72  
 
 73  0
         books = new Book[1];
 74  0
         books[0] = book;
 75  0
     }
 76  
 
 77  
     /**
 78  
      * Create BookData for multiple books.
 79  
      * 
 80  
      * @param books the set of Books to which the data belongs
 81  
      * @param key the Key specifying the data
 82  
      * @param compare when true each pair of adjacent books is to be compared
 83  
      */
 84  0
     public BookData(Book[] books, Key key, boolean compare) {
 85  0
         assert books != null && books.length > 0;
 86  0
         assert key != null;
 87  
 
 88  0
         this.books = books.clone();
 89  0
         this.key = key;
 90  0
         this.comparingBooks = compare;
 91  0
     }
 92  
 
 93  
     /**
 94  
      * Accessor for the root OSIS element
 95  
      * 
 96  
      * @return the root of the OSIS document representing this data
 97  
      * @throws BookException if there is any problem with this request
 98  
      */
 99  
     public Element getOsis() throws BookException {
 100  0
         if (osis == null) {
 101  
             // TODO(DMS): Determine the proper representation of the OSISWork
 102  
             // name for multiple books.
 103  0
             osis = OSISUtil.createOsisFramework(getFirstBook().getBookMetaData());
 104  0
             Element text = osis.getChild(OSISUtil.OSIS_ELEMENT_OSISTEXT);
 105  0
             Element div = getOsisFragment();
 106  0
             text.addContent(div);
 107  
         }
 108  
 
 109  0
         return osis;
 110  
     }
 111  
 
 112  
     /**
 113  
      * Accessor for the requested data in OSIS format.
 114  
      * 
 115  
      * @return the fragment of the OSIS document representing this data
 116  
      * @throws BookException if there is any problem with this request
 117  
      */
 118  
     public Element getOsisFragment() throws BookException {
 119  0
         if (fragment == null) {
 120  0
             fragment = getOsisContent(true);
 121  
         }
 122  
 
 123  0
         return fragment;
 124  
     }
 125  
 
 126  
     /**
 127  
      * Accessor for the root OSIS element
 128  
      * 
 129  
      * @param allowGenTitles whether to generate titles
 130  
      * @return the root of the document
 131  
      * @throws BookException if there is any problem with this request
 132  
      */
 133  
     public Element getOsisFragment(boolean allowGenTitles) throws BookException {
 134  0
         if (fragment == null) {
 135  0
             fragment = getOsisContent(allowGenTitles);
 136  
         }
 137  
 
 138  0
         return fragment;
 139  
     }
 140  
 
 141  
     /**
 142  
      * Output the current data as a SAX stream.
 143  
      *
 144  
      * @return A way of posting SAX events
 145  
      * @throws BookException if there is any problem with this request
 146  
      */
 147  
     public SAXEventProvider getSAXEventProvider() throws BookException {
 148  
         // If the fragment is already in a document, then use that.
 149  0
         Element frag = getOsisFragment();
 150  0
         Document doc = frag.getDocument();
 151  0
         if (doc == null) {
 152  0
             doc = new Document(frag);
 153  
         }
 154  0
         return new JDOMSAXEventProvider(doc);
 155  
     }
 156  
 
 157  
     /**
 158  
      * Who created this data.
 159  
      *
 160  
      * @return Returns the book.
 161  
      */
 162  
     public Book[] getBooks() {
 163  0
         return books == null ? null : (Book[]) books.clone();
 164  
     }
 165  
 
 166  
     /**
 167  
      * Get the first book.
 168  
      * 
 169  
      * @return the first or only book
 170  
      */
 171  
     public Book getFirstBook() {
 172  0
         return books != null && books.length > 0 ? books[0] : null;
 173  
     }
 174  
 
 175  
     /**
 176  
      * The key used to obtain data from one or more books.
 177  
      *
 178  
      * @return Returns the key.
 179  
      */
 180  
     public Key getKey() {
 181  0
         return key;
 182  
     }
 183  
 
 184  
     /**
 185  
      * @return whether the books should be compared.
 186  
      */
 187  
     public boolean isComparingBooks() {
 188  0
         return comparingBooks;
 189  
     }
 190  
 
 191  
     private Element getOsisContent(boolean allowGenTitles) throws BookException {
 192  0
         Element div = OSISUtil.factory().createDiv();
 193  
 
 194  0
         if (books.length == 1) {
 195  0
             Iterator<Content> iter = books[0].getOsisIterator(key, false, allowGenTitles);
 196  0
             while (iter.hasNext()) {
 197  0
                 Content content = iter.next();
 198  0
                 div.addContent(content);
 199  0
             }
 200  0
         } else {
 201  0
             Element table = OSISUtil.factory().createTable();
 202  0
             Element row = OSISUtil.factory().createRow();
 203  0
             Element cell = OSISUtil.factory().createCell();
 204  
 
 205  0
             table.addContent(row);
 206  
 
 207  0
             Iterator<Content>[] iters = new Iterator[books.length];
 208  0
             Passage[] passages = new Passage[books.length];
 209  0
             boolean[] showDiffs = new boolean[books.length - 1];
 210  0
             boolean doDiffs = false;
 211  
 
 212  
             //iterate through a first time mapping out our data. This enables us to detect a difference in number
 213  
             //of ranges later on and flag it to the user...
 214  0
             boolean[] ommittedVerses = new boolean[books.length];
 215  0
             int numRangesInMasterPassage = 0;
 216  0
             for (int i = 0; i < books.length; i++) {
 217  
                 //although the osis iterator now caters for keys in different versifications
 218  
                 //we are going to want to analyse the resulting key, so let's do the conversion up-front
 219  0
                 passages[i] = VersificationsMapper.instance().map(KeyUtil.getPassage(key), getVersification(i));
 220  
 
 221  
                 //iterator takes care of versification differences here...
 222  0
                 iters[i] = books[i].getOsisIterator(passages[i], true, true);
 223  
 
 224  0
                 if (i == 0) {
 225  
                     //we never omit a verse for the first passage, since we're going to output everything based on that.
 226  0
                     ommittedVerses[i] = false;
 227  0
                     numRangesInMasterPassage = passages[i].countRanges(RestrictionType.NONE);
 228  
                 } else {
 229  
                     // basically, if we end up with more ranges than we started with, then we're omitting a verse
 230  
                     //somewhere along the lines.
 231  0
                     ommittedVerses[i] = passages[i].countRanges(RestrictionType.NONE) > numRangesInMasterPassage;
 232  
                 }
 233  
             }
 234  
 
 235  
 
 236  
             //now read the content and map it out
 237  0
             BookVerseContent[] booksContents = new BookVerseContent[books.length];
 238  0
             for (int i = 0; i < books.length; i++) {
 239  0
                 doDiffs |= addHeaderAndSetShowDiffsState(row, showDiffs, i, ommittedVerses[i]);
 240  0
                 booksContents[i] = keyIteratorContentByVerse(
 241  
                         getVersification(i),
 242  
                         iters[i]);
 243  
             }
 244  
 
 245  0
             int cellCount = 0;
 246  0
             int rowCount = 0;
 247  
 
 248  
             //we iterate through the first book's contents, and match the verses from all the other ones
 249  0
             for (Map.Entry<Verse, List<Content>> verseContent : booksContents[0].entrySet()) {
 250  0
                 cellCount = 0;
 251  0
                 row = OSISUtil.factory().createRow();
 252  0
                 String firstText = "";
 253  
 
 254  0
                 for (int i = 0; i < books.length; i++) {
 255  0
                     Book book = books[i];
 256  0
                     cell = OSISUtil.factory().createCell();
 257  0
                     Language lang = book.getLanguage();
 258  0
                     if (lang != null) {
 259  0
                         cell.setAttribute(OSISUtil.OSIS_ATTR_LANG, lang.getCode(), Namespace.XML_NAMESPACE);
 260  
                     }
 261  
 
 262  0
                     row.addContent(cell);
 263  
 
 264  0
                     StringBuilder newText = new StringBuilder(doDiffs ? 32 : 0);
 265  
 
 266  
                     //get the contents from the mapped verse - key might be null if we had content outside of a verse.
 267  
                     //might be a no-op if it's in the same versification.
 268  0
                     Key verseInRelavantBookContents = VersificationsMapper.instance().mapVerse(verseContent.getKey(), getVersification(i));
 269  
 
 270  
                     //key might have several child keys, ie. a verse mapping to a range, or list of verses
 271  0
                     Passage passageOfInterest = KeyUtil.getPassage(verseInRelavantBookContents);
 272  0
                     Iterator<Key> passageKeys = passageOfInterest.iterator();
 273  0
                     while (passageKeys.hasNext()) {
 274  0
                         Key singleKey = passageKeys.next();
 275  
                         //TODO(CJB): for performance, we probably want to avoid the instanceof, so either change the
 276  
                         //method signature, or cast directly and be optimistic
 277  0
                         if (!(singleKey instanceof Verse)) {
 278  0
                             throw new UnsupportedOperationException("Iterating through a passage gives non-verses");
 279  
                         }
 280  
 
 281  0
                         List<Content> xmlContent = booksContents[i].get(singleKey);
 282  
 
 283  
                         //if the book simply did not contain that reference (say Greek book, with Gen.1 as a reference)
 284  
                         //then we end up with a key that doesn't exist in the map. Therefore, we need to cope for this.
 285  0
                         if (xmlContent == null) {
 286  0
                             xmlContent = new ArrayList<Content>(0);
 287  
                         }
 288  
 
 289  0
                         addText(doDiffs, newText, xmlContent);
 290  
 
 291  0
                         if (doDiffs) {
 292  0
                             String thisText = newText.toString();
 293  0
                             if (unaccenter != null) {
 294  0
                                 thisText = unaccenter.unaccent(thisText);
 295  
                             }
 296  
 
 297  0
                             if (i > 0 && showDiffs[i - 1]) {
 298  0
                                 List<Difference> diffs = new Diff(firstText, thisText, false).compare();
 299  0
                                 DiffCleanup.cleanupSemantic(diffs);
 300  0
                                 cell.addContent(OSISUtil.diffToOsis(diffs));
 301  
 
 302  
                                 // Since we used that cell create another
 303  0
                                 cell = OSISUtil.factory().createCell();
 304  0
                                 lang = book.getLanguage();
 305  0
                                 cell.setAttribute(OSISUtil.OSIS_ATTR_LANG, lang.getCode(), Namespace.XML_NAMESPACE);
 306  0
                                 row.addContent(cell);
 307  
                             }
 308  0
                             if (i == 0) {
 309  0
                                 firstText = thisText;
 310  
                             }
 311  
                         }
 312  
 
 313  
                         //TODO(CJB): wrong location - we should record the keys in a set and notify
 314  
                         //when there is a problem
 315  
                         //this should be outside of the loop?
 316  0
                         addContentSafely(cell, xmlContent);
 317  0
                         cellCount++;
 318  0
                     }
 319  
                 }
 320  
 
 321  0
                 if (cellCount == 0) {
 322  0
                     break;
 323  
                 }
 324  
 
 325  0
                 table.addContent(row);
 326  0
                 rowCount++;
 327  0
             }
 328  0
             if (rowCount > 0) {
 329  0
                 div.addContent(table);
 330  
             }
 331  
         }
 332  
 
 333  0
         return div;
 334  
     }
 335  
 
 336  
     /**
 337  
      * JDOM will throw an exception if we try and add the content to multiple parents.
 338  
      * As a result, we take the opportunity to add it safely, and add a note indicating
 339  
      * this content appears twice.
 340  
      *
 341  
      * @param cell the element to be added
 342  
      * @param xmlContent the collector of content
 343  
      */
 344  
     private void addContentSafely(final Element cell, final List<Content> xmlContent) {
 345  0
         Element note = null;
 346  0
         for (Content c : xmlContent) {
 347  0
             if (c.getParent() == null) {
 348  0
                 cell.addContent(c);
 349  0
             } else if (note != null) {
 350  0
                 note.addContent(c.clone());
 351  
             } else {
 352  
                 //we're in the situation where we have added this already.
 353  
                 //add note. In this case, we wrap the content that has already been applied.
 354  0
                 note = appendVersificationNotice(cell, "duplicate");
 355  0
                 note.addContent(c.clone());
 356  
             }
 357  
         }
 358  0
     }
 359  
 
 360  
     /**
 361  
      * Creates a notice element.
 362  
      *
 363  
      * @param parent the parent to which the notice is added
 364  
      * @param notice the notice fragment to be applied to the sub-type
 365  
      * @return the new element
 366  
      */
 367  
     private Element appendVersificationNotice(Element parent, final String notice) {
 368  0
         Element note = OSISUtil.factory().createDiv();
 369  0
         note.setAttribute(OSISUtil.OSIS_ATTR_TYPE, OSISUtil.GENERATED_CONTENT);
 370  0
         note.setAttribute(OSISUtil.OSIS_ATTR_SUBTYPE, OSISUtil.TYPE_X_PREFIX + notice);
 371  0
         parent.addContent(note);
 372  0
         return note;
 373  
     }
 374  
 
 375  
     /**
 376  
      * @param i the current position in the array of books
 377  
      * @return the versification of the book.
 378  
      */
 379  
     private Versification getVersification(final int i) {
 380  0
         return Versifications.instance().getVersification(
 381  
                 books[i].getBookMetaData().getProperty(BookMetaData.KEY_VERSIFICATION));
 382  
     }
 383  
 
 384  
 
 385  
     /**
 386  
      * We iterate through the content, making sure we key together those bits that belong together.
 387  
      * And separating out each verse.
 388  
      *
 389  
      * @param v11n the versification for the content
 390  
      * @param iter the iterator of OSIS content
 391  
      * @return the verse content for the book
 392  
      * @throws BookException if there is any problem with this request
 393  
      */
 394  
     private BookVerseContent keyIteratorContentByVerse(Versification v11n, final Iterator<Content> iter) throws BookException {
 395  0
         BookVerseContent contentsByOsisID = new BookVerseContent();
 396  
 
 397  
         //we will be using this map later to track whi ch keys have been catered for in the order calculation
 398  0
         Verse currentVerse = null;
 399  
         Content content;
 400  
 
 401  0
         List<Content> contents = new ArrayList<Content>();
 402  0
         while (iter.hasNext()) {
 403  0
             content = iter.next();
 404  0
             if (content instanceof Element && OSISUtil.OSIS_ELEMENT_VERSE.equals(((Element) content).getName())) {
 405  0
                 if (currentVerse != null) {
 406  0
                     contentsByOsisID.put(currentVerse, contents);
 407  0
                     contents = new ArrayList<Content>();
 408  
                 }
 409  
 
 410  0
                 currentVerse = OSISUtil.getVerse(v11n, (Element) content);
 411  
 
 412  
                 //if we still have stuff in here, then let's assign it to the previous verse (i.e.
 413  
                 //we might have come across content that legitimately sits in verse 0 for example).
 414  
                 //of perhaps we've somehow come across previous content. Either way, it clearly doesn't
 415  
                 //belong to the current verse.
 416  0
                 if (contents.size() > 0) {
 417  0
                     Verse previousVerse = new Verse(currentVerse.getVersification(), currentVerse.getOrdinal() - 1);
 418  0
                     contentsByOsisID.put(previousVerse, contents);
 419  0
                     contents = new ArrayList<Content>();
 420  
                 }
 421  
             }
 422  
 
 423  0
             contents.add(content);
 424  
         }
 425  
 
 426  
         //now append what's left into the last verse
 427  0
         if (currentVerse != null) {
 428  0
             contentsByOsisID.put(currentVerse, contents);
 429  
         }
 430  
 
 431  0
         return contentsByOsisID;
 432  
     }
 433  
 
 434  
     /**
 435  
      * @param row           our current OSIS row
 436  
      * @param showDiffs     the array of states as to whether we are showing diffs for this column
 437  
      * @param i             our current place in the state
 438  
      * @param ommittedVerse true to indicate this column will be ommiting a verse
 439  
      * @return true if we are doing diffs
 440  
      */
 441  
     private boolean addHeaderAndSetShowDiffsState(final Element row, final boolean[] showDiffs, final int i, final boolean ommittedVerse) {
 442  0
         boolean doDiffs = false;
 443  0
         Book book = books[i];
 444  0
         Element cell = OSISUtil.factory().createHeaderCell();
 445  
 
 446  0
         if (i > 0) {
 447  0
             Book firstBook = books[0];
 448  0
             BookCategory category = book.getBookCategory();
 449  
 
 450  0
             BookCategory prevCategory = firstBook.getBookCategory();
 451  0
             String prevName = firstBook.getInitials();
 452  0
             showDiffs[i - 1] = comparingBooks && BookCategory.BIBLE.equals(category) && category.equals(prevCategory)
 453  
                     && book.getLanguage().equals(firstBook.getLanguage()) && !book.getInitials().equals(prevName);
 454  
 
 455  0
             if (showDiffs[i - 1]) {
 456  0
                 doDiffs = true;
 457  0
                 StringBuilder buf = new StringBuilder(firstBook.getInitials());
 458  0
                 buf.append(" ==> ");
 459  0
                 buf.append(book.getInitials());
 460  
 
 461  0
                 cell.addContent(OSISUtil.factory().createText(buf.toString()));
 462  0
                 row.addContent(cell);
 463  0
                 cell = OSISUtil.factory().createHeaderCell();
 464  
             }
 465  
         }
 466  
 
 467  0
         final Text text = OSISUtil.factory().createText(book.getInitials());
 468  0
         if (ommittedVerse) {
 469  0
             Element notice = this.appendVersificationNotice(cell, "omitted-verses");
 470  0
             notice.addContent(text);
 471  0
         } else {
 472  0
             cell.addContent(text);
 473  
         }
 474  0
         row.addContent(cell);
 475  0
         return doDiffs;
 476  
     }
 477  
 
 478  
     /**
 479  
      * Loops around contents and calls addText for a single element
 480  
      *
 481  
      * @param doDiffs  true for calculating differences
 482  
      * @param newText  the newText buffer used to compare one portion of text to another
 483  
      * @param contents the contents to be added
 484  
      */
 485  
     private void addText(boolean doDiffs, StringBuilder newText, List<Content> contents) {
 486  0
         for (Content c : contents) {
 487  0
             addText(doDiffs, newText, c);
 488  
         }
 489  0
     }
 490  
 
 491  
     /**
 492  
      * Adds the text to the diff buffer
 493  
      *
 494  
      * @param doDiffs true for calculating differences
 495  
      * @param newText the newText buffer used to compare one portion of text to another
 496  
      * @param content the content element to be added
 497  
      */
 498  
     private void addText(boolean doDiffs, StringBuilder newText, Content content) {
 499  0
         if (doDiffs) {
 500  
             // if we already have content, let's add a space to avoid chaining words together
 501  0
             if (newText.length() != 0) {
 502  0
                 newText.append(' ');
 503  
             }
 504  
 
 505  0
             if (content instanceof Element) {
 506  0
                 newText.append(OSISUtil.getCanonicalText((Element) content));
 507  0
             } else if (content instanceof Text) {
 508  0
                 newText.append(((Text) content).getText());
 509  
             }
 510  
         }
 511  0
     }
 512  
 
 513  
     /**
 514  
      * @param unaccenter the unaccenter to set
 515  
      */
 516  
     public void setUnaccenter(UnAccenter unaccenter) {
 517  0
         this.unaccenter = unaccenter;
 518  0
     }
 519  
 
 520  
     /**
 521  
      * A temporary holder for a map that links each verse ID to its set of OSIS elements.
 522  
      * Used purely to avoid having too many generic/array notations entangled in the code
 523  
      */
 524  0
     class BookVerseContent extends TreeMap<Verse, List<Content>> {
 525  
         /**
 526  
          * Serialization ID
 527  
          */
 528  
         private static final long serialVersionUID = -6508118172314227362L;
 529  
     }
 530  
 
 531  
     /**
 532  
      * What key was used to create this data
 533  
      */
 534  
     private Key key;
 535  
 
 536  
     /**
 537  
      * The books to which the key should be applied.
 538  
      */
 539  
     private Book[] books;
 540  
 
 541  
     /**
 542  
      * Whether the Books should be compared.
 543  
      */
 544  
     private boolean comparingBooks;
 545  
 
 546  
     /**
 547  
      * The complete OSIS container for the element
 548  
      */
 549  
     private Element osis;
 550  
 
 551  
     /**
 552  
      * Just the element
 553  
      */
 554  
     private Element fragment;
 555  
 
 556  
     private UnAccenter unaccenter;
 557  
 }