Coverage Summary for Class: Paginator (org.kitodo.production.helper.metadata.pagination)

Class Class, % Method, % Line, %
Paginator 100% (1/1) 72,7% (8/11) 96,5% (138/143)


 /*
  * (c) Kitodo. Key to digital objects e. V. <contact@kitodo.org>
  *
  * This file is part of the Kitodo project.
  *
  * It is licensed under GNU General Public License version 3 or later.
  *
  * For the full copyright and license information, please read the
  * GPL3-License.txt file that was distributed with this source code.
  */
 
 package org.kitodo.production.helper.metadata.pagination;
 
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.Objects;
 
 /**
  * Class to generate different sorts of paginations.
  */
 public class Paginator implements Iterator<String> {
 
     /**
      * Fragments a pagination is composed of (text elements, counters, …).
      */
     private final LinkedList<Fragment> fragments = new LinkedList<>();
 
     /**
      * Supports rigth-to-left double page counting (2 1, 4 3, 6 5, …).
      */
     private boolean operateReverse = false;
 
     /**
      * Current counter value.
      */
     private HalfInteger value;
 
     private void parse(String initializer) {
 
         StringBuilder stringBuilder = new StringBuilder();
         PaginatorState paginatorState = PaginatorState.EMPTY;
 
         /*
          * iterate through the code points of the initializer string plus one more
          * iteration to process the last content of the stringBuilder
          */
         Boolean page = null;
         int length = initializer.length();
         for (int offset = 0; offset <= length;) {
             int codePoint;
             PaginatorState codePointClass;
             if (offset == length) {
                 codePointClass = PaginatorState.END;
                 codePoint = 0;
             } else {
                 codePoint = initializer.codePointAt(offset);
                 codePointClass = codePointClassOf(codePoint);
             }
 
             // Whatever is in back-ticks is not interpreted
             if (codePointClass.equals(PaginatorState.TEXT_ESCAPE_TRANSITION)) {
                 if (paginatorState.equals(PaginatorState.EMPTY)) {
                     paginatorState = PaginatorState.TEXT_ESCAPE_TRANSITION;
                 } else {
                     createFragment(stringBuilder, paginatorState, page);
                     page = null;
                     paginatorState = paginatorState.equals(PaginatorState.TEXT_ESCAPE_TRANSITION) ? PaginatorState.EMPTY
                             : PaginatorState.TEXT_ESCAPE_TRANSITION;
                 }
             } else if (paginatorState.equals(PaginatorState.TEXT_ESCAPE_TRANSITION)) {
                 stringBuilder.appendCodePoint(codePoint);
             } else if (codePointClass.equals(PaginatorState.HALF_INTEGER)
                     || codePointClass.equals(PaginatorState.FULL_INTEGER)) {
                 /*
                  * Recto/verso-only symbols cause a stringBuilder write (or they would be
                  * applied to the current stringBuilder content (modify their left side), but
                  * they shall be applied on the next write (modify their right side)). They set
                  * the page variable and are not written to the stringBuilder by themselves.
                  */
                 if (!paginatorState.equals(PaginatorState.EMPTY)) {
                     createFragment(stringBuilder, paginatorState, page);
                     paginatorState = PaginatorState.EMPTY;
                 }
                 page = codePointClass.equals(PaginatorState.HALF_INTEGER);
             } else if (paginatorState.equals(codePointClass) || paginatorState.equals(PaginatorState.EMPTY)
                     || paginatorState.equals(PaginatorState.TEXT) && codePointClass.equals(PaginatorState.SYMBOL)
                     || paginatorState.equals(PaginatorState.SYMBOL) && codePointClass.equals(PaginatorState.TEXT)) {
                 /*
                  * If the stringBuilder is empty or contains the same sort of content as the
                  * current input, just write it to the stringBuilder. If the stringBuilder
                  * contains text, we can write symbols as well, the same is true the other way
                  * ‘round.
                  */
                 stringBuilder.appendCodePoint(codePoint);
                 paginatorState = codePointClass;
 
             } else if (paginatorState.equals(PaginatorState.TEXT)
                     && (codePointClass.equals(PaginatorState.LOWERCASE_ROMAN)
                             || codePointClass.equals(PaginatorState.UPPERCASE_ROMAN))
                     || (paginatorState.equals(PaginatorState.LOWERCASE_ROMAN)
                             || paginatorState.equals(PaginatorState.UPPERCASE_ROMAN))
                             && codePointClass.equals(PaginatorState.TEXT)) {
                 /*
                  * If we got text, and the content of the stringBuilder is a Roman numeral, or
                  * the other way round, we can still write to the stringBuilder, but the result
                  * of the operation is always text. This is an important catch in order to, for
                  * example, prevent the C in ‘Chapter’ start counting. (Remember, Roman numeral
                  * C is 100.)
                  */
                 stringBuilder.appendCodePoint(codePoint);
                 paginatorState = PaginatorState.TEXT;
 
             } else {
                 // In any other case, we have to write out the stringBuilder.
                 createFragment(stringBuilder, paginatorState, page);
                 page = null;
                 stringBuilder.appendCodePoint(codePoint);
                 paginatorState = codePointClass;
             }
             offset += Character.charCount(codePoint);
         }
     }
 
     /**
      * Stores the stringBuilder as fragment and resets the stringBuilder. The type
      * of fragment is derived from the the stringBuilder’s PaginatorState and the
      * page directive. A page directive causes what is immediately thereafter to be
      * treated as text in any case.
      *
      * @param stringBuilder
      *            characters
      * @param fragmentType
      *            type of fragment to create
      * @param pageType
      *            page information
      */
     private void createFragment(StringBuilder stringBuilder, PaginatorState fragmentType, Boolean pageType) {
         if (pageType == null && fragmentType.equals(PaginatorState.DECIMAL)) {
             fragments.addLast(new DecimalNumeral(stringBuilder.toString()));
         } else if (pageType == null && (fragmentType.equals(PaginatorState.UPPERCASE_ROMAN)
                 || fragmentType.equals(PaginatorState.LOWERCASE_ROMAN))) {
             fragments.addLast(
                 new RomanNumeral(stringBuilder.toString(), fragmentType.equals(PaginatorState.UPPERCASE_ROMAN)));
         } else if (fragmentType.equals(PaginatorState.INCREMENT)) {
             if (fragments.isEmpty() || Objects.isNull(fragments.peekLast())) {
                 fragments.addLast(new StaticText("", pageType));
             }
             fragments.peekLast().setIncrement(HalfInteger.valueOf(stringBuilder.toString()));
         } else if (!fragmentType.equals(PaginatorState.EMPTY)) {
             fragments.addLast(new StaticText(stringBuilder.toString(), pageType));
         }
         stringBuilder.setLength(0);
     }
 
     /**
      * Creates a new paginator.
      *
      * @param initializer
      *            initial value
      */
     public Paginator(String initializer) {
 
         /*
          * If the initialisation string starts with a half increment marker this
          * increments the initial counter value by one half. This is to allow starting
          * with an interemdiate (verso) subpage.
          */
         boolean halfAboveValue = !initializer.isEmpty() && initializer.codePointAt(0) == '½';
         String paginatorInitializer = halfAboveValue ? initializer.substring(1) : initializer;
         parse(paginatorInitializer);
         initializeIncrements(halfAboveValue);
     }
 
     /**
      * Initializes missing increments, finds the initial value and, optional, a
      * reverse operation mode.
      */
     private void initializeIncrements(boolean aHalf) {
         Fragment firstFragment = null;
         Fragment lastFragment = null;
         int valueFull;
 
         for (Fragment fragment : fragments) {
             if (fragment.getInitialValue() == null) {
                 continue;
             }
             if (firstFragment == null) {
                 firstFragment = fragment;
             }
             lastFragment = fragment;
         }
         if (firstFragment == null) { // static text only
             valueFull = 0;
         } else if (firstFragment.equals(lastFragment)) { // only one counting element
             valueFull = firstFragment.getInitialValue();
             if (firstFragment.getIncrement() == null) {
                 firstFragment.setIncrement(new HalfInteger(1, false));
             }
         } else if (firstFragment.getInitialValue() <= lastFragment.getInitialValue()) {
             valueFull = initializeLeftToRightMode(firstFragment, lastFragment);
         } else {
             valueFull = initializeRightToLeftMode(firstFragment, lastFragment);
         }
         value = new HalfInteger(valueFull, aHalf);
     }
 
     /**
      * More than one counting element in left-to-right order.
      */
     private int initializeLeftToRightMode(Fragment firstFragment, Fragment lastFragment) {
         int valueFull;
         valueFull = firstFragment.getInitialValue();
         Fragment previousFragment = null;
         int howMany = 0;
         for (Fragment fragment : fragments) {
             if (fragment.getInitialValue() == null) {
                 continue;
             }
 
             if (previousFragment != null && previousFragment.getIncrement() == null) {
                 previousFragment.setIncrement(
                     new HalfInteger(fragment.getInitialValue() - previousFragment.getInitialValue(), false));
             }
 
             previousFragment = fragment;
             howMany++;
         }
         if (lastFragment.getIncrement() == null) {
             lastFragment.setIncrement(new HalfInteger(
                     (lastFragment.getInitialValue() - firstFragment.getInitialValue()) / (howMany - 1), false));
         }
         return valueFull;
     }
 
     /**
      * More than one counting element in right-to-left order.
      */
     private int initializeRightToLeftMode(Fragment firstFragment, Fragment lastFragment) {
         int valueFull;
         this.operateReverse = true;
         valueFull = lastFragment.getInitialValue();
         Fragment previousFragment = null;
         int howMany = 0;
         for (Iterator<Fragment> iterator = fragments.descendingIterator(); iterator.hasNext();) {
             Fragment fragment = iterator.next();
 
             if (fragment.getInitialValue() == null) {
                 continue;
             }
 
             if (previousFragment != null && previousFragment.getIncrement() == null) {
                 previousFragment.setIncrement(
                     new HalfInteger(fragment.getInitialValue() - previousFragment.getInitialValue(), false));
             }
             previousFragment = fragment;
             howMany++;
         }
         if (firstFragment.getIncrement() == null) {
             firstFragment.setIncrement(new HalfInteger(
                     (firstFragment.getInitialValue() - lastFragment.getInitialValue()) / (howMany - 1), false));
         }
         return valueFull;
     }
 
     private static PaginatorState codePointClassOf(int codePoint) {
         switch (codePoint) {
             case '0': case '1': case '2': case '3': case '4': case '5':
             case '6': case '7': case '8': case '9':
                 return PaginatorState.DECIMAL;
             case 'C': case 'D': case 'I': case 'L': case 'M': case 'V':
             case 'X':
                 return PaginatorState.UPPERCASE_ROMAN;
             case '`':
                 return PaginatorState.TEXT_ESCAPE_TRANSITION;
             case 'c': case 'd': case 'i': case 'l': case 'm': case 'v':
             case 'x':
                 return PaginatorState.LOWERCASE_ROMAN;
             case '¡':
                 return PaginatorState.FULL_INTEGER;
             case '°': case '²': case '³': case '¹': case '½':
                 return PaginatorState.INCREMENT;
             case '¿':
                 return PaginatorState.HALF_INTEGER;
             default:
                 switch (Character.getType(codePoint)) {
                     case Character.UPPERCASE_LETTER:
                     case Character.LOWERCASE_LETTER:
                     case Character.TITLECASE_LETTER:
                     case Character.MODIFIER_LETTER:
                     case Character.OTHER_LETTER:
                     case Character.NON_SPACING_MARK:
                         return PaginatorState.TEXT;
                     default:
                         return PaginatorState.SYMBOL;
                 }
         }
     }
 
     /**
      * To prevent infinite loops by using hasNext as condition, a
      * UnsupportedOperationException is thrown because there are always next
      * elements.
      */
     @Override
     public boolean hasNext() {
         throw new UnsupportedOperationException("Paginator.hasNext()");
     }
 
     @Override
     public String next() {
         StringBuilder result = new StringBuilder();
         if (operateReverse) {
             for (Iterator<Fragment> iterator = fragments.descendingIterator(); iterator.hasNext();) {
                 Fragment fragment = iterator.next();
                 result.insert(0, fragment.format(value));
                 value = value.add(fragment.getIncrement());
             }
         } else {
             for (Fragment fragment : fragments) {
                 result.append(fragment.format(value));
                 value = value.add(fragment.getIncrement());
             }
         }
         return result.toString();
     }
 
     /**
      * The iterator does not support {@code remove()}.
      *
      * @throws UnsupportedOperationException
      *             if invoked.
      */
     @Override
     public void remove() {
         throw new UnsupportedOperationException("Paginator.remove()");
 
     }
 
     /**
      * Returns a concise string representation of this instance.
      *
      * @return a string representing this instance
      */
     @Override
     public String toString() {
         return value + (operateReverse ? ", reversed, " : ", ") + fragments;
     }
 }