/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* $Id: FOText.java 1805173 2017-08-16 10:50:04Z ssteiner $ */

package org.apache.fop.fo;

import java.awt.Color;
import java.nio.CharBuffer;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.NoSuchElementException;
import java.util.Stack;

import org.xml.sax.Locator;

import org.apache.fop.accessibility.StructureTreeElement;
import org.apache.fop.apps.FOPException;
import org.apache.fop.complexscripts.bidi.DelimitedTextRange;
import org.apache.fop.datatypes.Length;
import org.apache.fop.fo.flow.Block;
import org.apache.fop.fo.properties.CommonFont;
import org.apache.fop.fo.properties.CommonHyphenation;
import org.apache.fop.fo.properties.CommonTextDecoration;
import org.apache.fop.fo.properties.KeepProperty;
import org.apache.fop.fo.properties.Property;
import org.apache.fop.fo.properties.SpaceProperty;
import org.apache.fop.fonts.TextFragment;
import org.apache.fop.util.CharUtilities;

A text node (PCDATA) in the formatting object tree.
/** * A text node (PCDATA) in the formatting object tree. */
public class FOText extends FONode implements CharSequence, TextFragment {
the CharBuffer containing the text
/** the <code>CharBuffer</code> containing the text */
private CharBuffer charBuffer; // cached iterator private CharacterIterator charIterator; // The value of FO traits (refined properties) that apply to #PCDATA // (aka implicit sequence of fo:character) private CommonFont commonFont; private CommonHyphenation commonHyphenation; private Color color; private KeepProperty keepTogether; private Property letterSpacing; private SpaceProperty lineHeight; private int whiteSpaceTreatment; private int whiteSpaceCollapse; private int textTransform; private Property wordSpacing; private int wrapOption; private Length baselineShift; private String country; private String language; private String script; // End of trait values
Points to the previous FOText object created within the current block. If this is "null", this is the first such object.
/** * Points to the previous FOText object created within the current * block. If this is "null", this is the first such object. */
private FOText prevFOTextThisBlock;
Points to the next FOText object created within the current block. If this is "null", this is the last such object.
/** * Points to the next FOText object created within the current * block. If this is "null", this is the last such object. */
private FOText nextFOTextThisBlock;
Points to the ancestor Block object. This is used to keep track of which FOText nodes are descendants of the same block.
/** * Points to the ancestor Block object. This is used to keep track of * which FOText nodes are descendants of the same block. */
private Block ancestorBlock;
Holds the text decoration values. May be null
/** Holds the text decoration values. May be null */
private CommonTextDecoration textDecoration; private StructureTreeElement structureTreeElement; /* bidi levels */ private int[] bidiLevels; private static final int IS_WORD_CHAR_FALSE = 0; private static final int IS_WORD_CHAR_TRUE = 1; private static final int IS_WORD_CHAR_MAYBE = 2;
Creates a new FO text node.
Params:
  • parent – FONode that is the parent of this object
/** * Creates a new FO text node. * * @param parent FONode that is the parent of this object */
public FOText(FONode parent) { super(parent); }
{@inheritDoc}
/** {@inheritDoc} */
protected void characters(char[] data, int start, int length, PropertyList list, Locator locator) throws FOPException { if (charBuffer == null) { // buffer not yet initialized, do so now int newLength = (length < 16) ? 16 : length; charBuffer = CharBuffer.allocate(newLength); } else { // allocate a larger buffer, and transfer contents int requires = charBuffer.position() + length; int capacity = charBuffer.capacity(); if (requires > capacity) { int newCapacity = capacity * 2; if (requires > newCapacity) { newCapacity = requires; } CharBuffer newBuffer = CharBuffer.allocate(newCapacity); charBuffer.rewind(); newBuffer.put(charBuffer); charBuffer = newBuffer; } } // extend limit to capacity charBuffer.limit(charBuffer.capacity()); // append characters charBuffer.put(data, start, length); // shrink limit to position charBuffer.limit(charBuffer.position()); }
Return the array of characters for this instance.
Returns: a char sequence containing the text
/** * Return the array of characters for this instance. * * @return a char sequence containing the text */
public CharSequence getCharSequence() { if (this.charBuffer == null) { return null; } this.charBuffer.rewind(); return this.charBuffer.asReadOnlyBuffer().subSequence(0, this.charBuffer.limit()); }
{@inheritDoc}
/** {@inheritDoc} */
public FONode clone(FONode parent, boolean removeChildren) throws FOPException { FOText ft = (FOText) super.clone(parent, removeChildren); if (removeChildren) { // not really removing, just make sure the char buffer // pointed to is really a different one if (charBuffer != null) { ft.charBuffer = CharBuffer.allocate(charBuffer.limit()); charBuffer.rewind(); ft.charBuffer.put(charBuffer); ft.charBuffer.rewind(); } } ft.prevFOTextThisBlock = null; ft.nextFOTextThisBlock = null; ft.ancestorBlock = null; return ft; }
{@inheritDoc}
/** {@inheritDoc} */
public void bind(PropertyList pList) throws FOPException { this.commonFont = pList.getFontProps(); this.commonHyphenation = pList.getHyphenationProps(); this.color = pList.get(Constants.PR_COLOR).getColor(getUserAgent()); this.keepTogether = pList.get(Constants.PR_KEEP_TOGETHER).getKeep(); this.lineHeight = pList.get(Constants.PR_LINE_HEIGHT).getSpace(); this.letterSpacing = pList.get(Constants.PR_LETTER_SPACING); this.whiteSpaceCollapse = pList.get(Constants.PR_WHITE_SPACE_COLLAPSE).getEnum(); this.whiteSpaceTreatment = pList.get(Constants.PR_WHITE_SPACE_TREATMENT).getEnum(); this.textTransform = pList.get(Constants.PR_TEXT_TRANSFORM).getEnum(); this.wordSpacing = pList.get(Constants.PR_WORD_SPACING); this.wrapOption = pList.get(Constants.PR_WRAP_OPTION).getEnum(); this.textDecoration = pList.getTextDecorationProps(); this.baselineShift = pList.get(Constants.PR_BASELINE_SHIFT).getLength(); this.country = pList.get(Constants.PR_COUNTRY).getString(); this.language = pList.get(Constants.PR_LANGUAGE).getString(); this.script = pList.get(Constants.PR_SCRIPT).getString(); }
{@inheritDoc}
/** {@inheritDoc} */
public void endOfNode() throws FOPException { if (charBuffer != null) { charBuffer.rewind(); } super.endOfNode(); getFOEventHandler().characters(this); }
{@inheritDoc}
/** {@inheritDoc} */
public void finalizeNode() { textTransform(); }
Check if this text node will create an area. This means either there is non-whitespace or it is preserved whitespace. Maybe this just needs to check length > 0, since char iterators handle whitespace.
Returns:true if this will create an area in the output
/** * Check if this text node will create an area. * This means either there is non-whitespace or it is * preserved whitespace. * Maybe this just needs to check length &gt; 0, since char iterators * handle whitespace. * * @return true if this will create an area in the output */
public boolean willCreateArea() { if (whiteSpaceCollapse == Constants.EN_FALSE && charBuffer.limit() > 0) { return true; } char ch; charBuffer.rewind(); while (charBuffer.hasRemaining()) { ch = charBuffer.get(); if (!((ch == CharUtilities.SPACE) || (ch == CharUtilities.LINEFEED_CHAR) || (ch == CharUtilities.CARRIAGE_RETURN) || (ch == CharUtilities.TAB))) { // not whitespace charBuffer.rewind(); return true; } } return false; }
Returns:a new TextCharIterator
/** * @return a new TextCharIterator */
public CharIterator charIterator() { return new TextCharIterator(); }
This method is run as part of the ancestor Block's flushText(), to create xref pointers to the previous FOText objects within the same Block
Params:
  • ancestorBlock – the ancestor fo:block
/** * This method is run as part of the ancestor Block's flushText(), to * create xref pointers to the previous FOText objects within the same Block * @param ancestorBlock the ancestor fo:block */
protected void createBlockPointers(Block ancestorBlock) { this.ancestorBlock = ancestorBlock; // if the last FOText is a sibling, point to it, and have it point here if (ancestorBlock.lastFOTextProcessed != null) { if (ancestorBlock.lastFOTextProcessed.ancestorBlock == this.ancestorBlock) { prevFOTextThisBlock = ancestorBlock.lastFOTextProcessed; prevFOTextThisBlock.nextFOTextThisBlock = this; } else { prevFOTextThisBlock = null; } } }
This method is run as part of endOfNode(), to handle the text-transform property for accumulated FOText
/** * This method is run as part of endOfNode(), to handle the * text-transform property for accumulated FOText */
private void textTransform() { if (getBuilderContext().inMarker() || textTransform == Constants.EN_NONE) { return; } charBuffer.rewind(); CharBuffer tmp = charBuffer.slice(); char c; int lim = charBuffer.limit(); int pos = -1; while (++pos < lim) { c = charBuffer.get(); switch (textTransform) { case Constants.EN_UPPERCASE: tmp.put(Character.toUpperCase(c)); break; case Constants.EN_LOWERCASE: tmp.put(Character.toLowerCase(c)); break; case Constants.EN_CAPITALIZE: if (isStartOfWord(pos)) { /* Use toTitleCase here. Apparently, some languages use a different character to represent a letter when using initial caps than when all of the letters in the word are capitalized. We will try to let Java handle this. */ tmp.put(Character.toTitleCase(c)); } else { tmp.put(c); } break; default: //should never happen as the property subsystem catches that case assert false; //nop } } }
Determines whether a particular location in an FOText object's text is the start of a new "word". The use of "word" here is specifically for the text-transform property, but may be useful for other things as well, such as word-spacing. The definition of "word" is somewhat ambiguous and appears to be definable by the user agent.
Params:
  • i – index into charBuffer
Returns:True if the character at this location is the start of a new word.
/** * Determines whether a particular location in an FOText object's text is * the start of a new "word". The use of "word" here is specifically for * the text-transform property, but may be useful for other things as * well, such as word-spacing. The definition of "word" is somewhat ambiguous * and appears to be definable by the user agent. * * @param i index into charBuffer * * @return True if the character at this location is the start of a new * word. */
private boolean isStartOfWord(int i) { char prevChar = getRelativeCharInBlock(i, -1); /* All we are really concerned about here is of what type prevChar * is. If inputChar is not part of a word, then the Java * conversions will (we hope) simply return inputChar. */ switch (isWordChar(prevChar)) { case IS_WORD_CHAR_TRUE: return false; case IS_WORD_CHAR_FALSE: return true; /* "MAYBE" implies that additional context is needed. An example is a * single-quote, either straight or closing, which might be interpreted * as a possessive or a contraction, or might be a closing quote. */ case IS_WORD_CHAR_MAYBE: char prevPrevChar = getRelativeCharInBlock(i, -2); switch (isWordChar(prevPrevChar)) { case IS_WORD_CHAR_TRUE: return false; case IS_WORD_CHAR_FALSE: return true; case IS_WORD_CHAR_MAYBE: return true; default: return false; } default: return false; } }
Finds a character within the current Block that is relative in location to a character in the current FOText. Treats all FOText objects within a block as one unit, allowing text in adjoining FOText objects to be returned if the parameters are outside of the current object.
Params:
  • i – index into the CharBuffer
  • offset – signed integer with relative position within the block of the character to return. To return the character immediately preceding i, pass -1. To return the character immediately after i, pass 1.
Returns:the character in the offset position within the block; \u0000 if the offset points to an area outside of the block.
/** * Finds a character within the current Block that is relative in location * to a character in the current FOText. Treats all FOText objects within a * block as one unit, allowing text in adjoining FOText objects to be * returned if the parameters are outside of the current object. * * @param i index into the CharBuffer * @param offset signed integer with relative position within the * block of the character to return. To return the character immediately * preceding i, pass -1. To return the character immediately after i, * pass 1. * @return the character in the offset position within the block; \u0000 if * the offset points to an area outside of the block. */
private char getRelativeCharInBlock(int i, int offset) { int charIndex = i + offset; // The easy case is where the desired character is in the same FOText if (charIndex >= 0 && charIndex < this.length()) { return this.charAt(i + offset); } // For now, we can't look at following FOText nodes if (offset > 0) { return CharUtilities.NULL_CHAR; } // Remaining case has the text in some previous FOText node boolean foundChar = false; char charToReturn = CharUtilities.NULL_CHAR; FOText nodeToTest = this; int remainingOffset = offset + i; while (!foundChar) { if (nodeToTest.prevFOTextThisBlock == null) { break; } nodeToTest = nodeToTest.prevFOTextThisBlock; int diff = nodeToTest.length() + remainingOffset - 1; if (diff >= 0) { charToReturn = nodeToTest.charAt(diff); foundChar = true; } else { remainingOffset += diff; } } return charToReturn; } /** * @return The previous FOText node in this Block; null, if this is the * first FOText in this Block. */ //public FOText getPrevFOTextThisBlock () { // return prevFOTextThisBlock; //} /** * @return The next FOText node in this Block; null if this is the last * FOText in this Block; null if subsequent FOText nodes have not yet been * processed. */ //public FOText getNextFOTextThisBlock () { // return nextFOTextThisBlock; //} /** * @return The nearest ancestor block object which contains this FOText. */ //public Block getAncestorBlock () { // return ancestorBlock; //}
Determines whether the input char should be considered part of a "word". This is used primarily to determine whether the character immediately following starts a new word, but may have other uses. We have not found a definition of "word" in the standard (1.0), so the logic used here is based on the programmer's best guess.
Params:
  • inputChar – the character to be tested.
Returns:int IS_WORD_CHAR_TRUE, IS_WORD_CHAR_FALSE, or IS_WORD_CHAR_MAYBE, depending on whether the character should be considered part of a word or not.
/** * Determines whether the input char should be considered part of a * "word". This is used primarily to determine whether the character * immediately following starts a new word, but may have other uses. * We have not found a definition of "word" in the standard (1.0), so the * logic used here is based on the programmer's best guess. * * @param inputChar the character to be tested. * @return int IS_WORD_CHAR_TRUE, IS_WORD_CHAR_FALSE, or IS_WORD_CHAR_MAYBE, * depending on whether the character should be considered part of a word * or not. */
private static int isWordChar(char inputChar) { switch (Character.getType(inputChar)) { case Character.COMBINING_SPACING_MARK: return IS_WORD_CHAR_TRUE; case Character.CONNECTOR_PUNCTUATION: return IS_WORD_CHAR_TRUE; case Character.CONTROL: return IS_WORD_CHAR_FALSE; case Character.CURRENCY_SYMBOL: return IS_WORD_CHAR_TRUE; case Character.DASH_PUNCTUATION: if (inputChar == '-') { return IS_WORD_CHAR_TRUE; //hyphen } return IS_WORD_CHAR_FALSE; case Character.DECIMAL_DIGIT_NUMBER: return IS_WORD_CHAR_TRUE; case Character.ENCLOSING_MARK: return IS_WORD_CHAR_FALSE; case Character.END_PUNCTUATION: if (inputChar == '\u2019') { return IS_WORD_CHAR_MAYBE; //apostrophe, right single quote } return IS_WORD_CHAR_FALSE; case Character.FORMAT: return IS_WORD_CHAR_FALSE; case Character.LETTER_NUMBER: return IS_WORD_CHAR_TRUE; case Character.LINE_SEPARATOR: return IS_WORD_CHAR_FALSE; case Character.LOWERCASE_LETTER: return IS_WORD_CHAR_TRUE; case Character.MATH_SYMBOL: return IS_WORD_CHAR_FALSE; case Character.MODIFIER_LETTER: return IS_WORD_CHAR_TRUE; case Character.MODIFIER_SYMBOL: return IS_WORD_CHAR_TRUE; case Character.NON_SPACING_MARK: return IS_WORD_CHAR_TRUE; case Character.OTHER_LETTER: return IS_WORD_CHAR_TRUE; case Character.OTHER_NUMBER: return IS_WORD_CHAR_TRUE; case Character.OTHER_PUNCTUATION: if (inputChar == '\'') { return IS_WORD_CHAR_MAYBE; //ASCII apostrophe } return IS_WORD_CHAR_FALSE; case Character.OTHER_SYMBOL: return IS_WORD_CHAR_TRUE; case Character.PARAGRAPH_SEPARATOR: return IS_WORD_CHAR_FALSE; case Character.PRIVATE_USE: return IS_WORD_CHAR_FALSE; case Character.SPACE_SEPARATOR: return IS_WORD_CHAR_FALSE; case Character.START_PUNCTUATION: return IS_WORD_CHAR_FALSE; case Character.SURROGATE: return IS_WORD_CHAR_FALSE; case Character.TITLECASE_LETTER: return IS_WORD_CHAR_TRUE; case Character.UNASSIGNED: return IS_WORD_CHAR_FALSE; case Character.UPPERCASE_LETTER: return IS_WORD_CHAR_TRUE; default: return IS_WORD_CHAR_FALSE; } } private class TextCharIterator extends CharIterator { private int currentPosition; private boolean canRemove; private boolean canReplace; public TextCharIterator() { }
{@inheritDoc}
/** {@inheritDoc} */
public boolean hasNext() { return (this.currentPosition < charBuffer.limit()); }
{@inheritDoc}
/** {@inheritDoc} */
public char nextChar() { if (this.currentPosition < charBuffer.limit()) { this.canRemove = true; this.canReplace = true; return charBuffer.get(currentPosition++); } else { throw new NoSuchElementException(); } }
{@inheritDoc}
/** {@inheritDoc} */
public void remove() { if (this.canRemove) { charBuffer.position(currentPosition); // Slice the buffer at the current position CharBuffer tmp = charBuffer.slice(); // Reset position to before current character charBuffer.position(--currentPosition); if (tmp.hasRemaining()) { // Transfer any remaining characters charBuffer.mark(); charBuffer.put(tmp); charBuffer.reset(); } // Decrease limit charBuffer.limit(charBuffer.limit() - 1); // Make sure following calls fail, unless nextChar() was called this.canRemove = false; } else { throw new IllegalStateException(); } }
{@inheritDoc}
/** {@inheritDoc} */
public void replaceChar(char c) { if (this.canReplace) { charBuffer.put(currentPosition - 1, c); } else { throw new IllegalStateException(); } } }
Returns:the Common Font Properties.
/** * @return the Common Font Properties. */
public CommonFont getCommonFont() { return commonFont; }
Returns:the Common Hyphenation Properties.
/** * @return the Common Hyphenation Properties. */
public CommonHyphenation getCommonHyphenation() { return commonHyphenation; }
Returns:the "color" trait.
/** * @return the "color" trait. */
public Color getColor() { return color; }
Returns:the "keep-together" trait.
/** * @return the "keep-together" trait. */
public KeepProperty getKeepTogether() { return keepTogether; }
Returns:the "letter-spacing" trait.
/** * @return the "letter-spacing" trait. */
public Property getLetterSpacing() { return letterSpacing; }
Returns:the "line-height" trait.
/** * @return the "line-height" trait. */
public SpaceProperty getLineHeight() { return lineHeight; }
Returns:the "white-space-treatment" trait
/** * @return the "white-space-treatment" trait */
public int getWhitespaceTreatment() { return whiteSpaceTreatment; }
Returns:the "word-spacing" trait.
/** * @return the "word-spacing" trait. */
public Property getWordSpacing() { return wordSpacing; }
Returns:the "wrap-option" trait.
/** * @return the "wrap-option" trait. */
public int getWrapOption() { return wrapOption; }
Returns:the "text-decoration" trait.
/** @return the "text-decoration" trait. */
public CommonTextDecoration getTextDecoration() { return textDecoration; }
Returns:the baseline-shift trait
/** @return the baseline-shift trait */
public Length getBaseLineShift() { return baselineShift; }
Returns:the country trait
/** @return the country trait */
public String getCountry() { return country; } @Override public synchronized CharacterIterator getIterator() { if (charIterator != null) { charIterator = new StringCharacterIterator(toString()); } return charIterator; } @Override public int getBeginIndex() { return 0; } @Override public int getEndIndex() { return length(); } @Override public String getLanguage() { return language; } @Override public String getScript() { return script; } @Override public int getBidiLevel() { return length() > 0 ? bidiLevelAt(0) : -1; }
{@inheritDoc}
/** {@inheritDoc} */
public String toString() { if (charBuffer == null) { return ""; } else { CharBuffer cb = charBuffer.duplicate(); cb.rewind(); return cb.toString(); } }
{@inheritDoc}
/** {@inheritDoc} */
public String getLocalName() { return "#PCDATA"; }
{@inheritDoc}
/** {@inheritDoc} */
public String getNormalNamespacePrefix() { return null; }
{@inheritDoc}
/** {@inheritDoc} */
protected String gatherContextInfo() { if (this.locator != null) { return super.gatherContextInfo(); } else { return this.toString(); } }
{@inheritDoc}
/** {@inheritDoc} */
public char charAt(int position) { return charBuffer.get(position); }
{@inheritDoc}
/** {@inheritDoc} */
public CharSequence subSequence(int start, int end) { return charBuffer.subSequence(start, end); }
{@inheritDoc}
/** {@inheritDoc} */
public int length() { return charBuffer.limit(); }
Resets the backing java.nio.CharBuffer
/** * Resets the backing <code>java.nio.CharBuffer</code> */
public void resetBuffer() { if (charBuffer != null) { charBuffer.rewind(); } } @Override public boolean isDelimitedTextRangeBoundary(int boundary) { return false; } @Override public void setStructureTreeElement(StructureTreeElement structureTreeElement) { this.structureTreeElement = structureTreeElement; } @Override public StructureTreeElement getStructureTreeElement() { return structureTreeElement; }
Set bidirectional level over interval [start,end).
Params:
  • level – the resolved level
  • start – the starting index of interval
  • end – the ending index of interval
/** * Set bidirectional level over interval [start,end). * @param level the resolved level * @param start the starting index of interval * @param end the ending index of interval */
public void setBidiLevel(int level, int start, int end) { if (start < end) { if (bidiLevels == null) { bidiLevels = new int [ length() ]; } for (int i = start, n = end; i < n; i++) { bidiLevels [ i ] = level; } if (parent != null) { ((FObj) parent).setBidiLevel(level); } } else { assert start < end; } }
Obtain bidirectional level of each character represented by this FOText.
Returns:a (possibly empty) array of bidi levels or null in case no bidi levels have been assigned
/** * Obtain bidirectional level of each character * represented by this FOText. * @return a (possibly empty) array of bidi levels or null * in case no bidi levels have been assigned */
public int[] getBidiLevels() { return bidiLevels; }
Obtain bidirectional level of each character over interval [start,end).
Params:
  • start – the starting index of interval
  • end – the ending index of interval
Returns:a (possibly empty) array of bidi levels or null in case no bidi levels have been assigned
/** * Obtain bidirectional level of each character over * interval [start,end). * @param start the starting index of interval * @param end the ending index of interval * @return a (possibly empty) array of bidi levels or null * in case no bidi levels have been assigned */
public int[] getBidiLevels(int start, int end) { if (this.bidiLevels != null) { assert start <= end; int n = end - start; int[] bidiLevels = new int [ n ]; System.arraycopy(this.bidiLevels, start + 0, bidiLevels, 0, n); return bidiLevels; } else { return null; } }
Obtain bidirectional level of character at specified position, which must be a non-negative integer less than the length of this FO.
Params:
  • position – an offset position into FO's characters
Throws:
Returns:a resolved bidi level or -1 if default
/** * Obtain bidirectional level of character at * specified position, which must be a non-negative integer * less than the length of this FO. * @param position an offset position into FO's characters * @return a resolved bidi level or -1 if default * @throws IndexOutOfBoundsException if position is not non-negative integer * or is greater than or equal to length */
public int bidiLevelAt(int position) throws IndexOutOfBoundsException { if ((position < 0) || (position >= length())) { throw new IndexOutOfBoundsException(); } else if (bidiLevels != null) { return bidiLevels [ position ]; } else { return -1; } } @Override protected Stack<DelimitedTextRange> collectDelimitedTextRanges(Stack<DelimitedTextRange> ranges, DelimitedTextRange currentRange) { if (currentRange != null) { currentRange.append(charIterator(), this); } return ranges; } private static class MapRange { private int start; private int end; MapRange(int start, int end) { this.start = start; this.end = end; } public int hashCode() { return (start * 31) + end; } public boolean equals(Object o) { if (o instanceof MapRange) { MapRange r = (MapRange) o; return (r.start == start) && (r.end == end); } else { return false; } } } }