blob: 7939deaf2c2184605bfe2c756f94d0a1951cc3c7 [file] [log] [blame]
/*
* 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.
*/
// BEGIN android-note
// changed from icu.text.Bidi to BidiWrapper
// END android-note
package java.text;
// BEGIN android-added
import java.awt.font.NumericShaper;
import java.awt.font.TextAttribute;
import java.util.Arrays;
import java.util.LinkedList;
import org.apache.harmony.text.BidiRun;
import org.apache.harmony.text.BidiWrapper;
// END android-added
import org.apache.harmony.text.internal.nls.Messages;
/**
* Provides the Unicode Bidirectional Algorithm. The algorithm is
* defined in the Unicode Standard Annex #9, version 13, also described in The
* Unicode Standard, Version 4.0 .
*
* Use a {@code Bidi} object to get the information on the position reordering of a
* bidirectional text, such as Arabic or Hebrew. The natural display ordering of
* horizontal text in these languages is from right to left, while they order
* numbers from left to right.
*
* If the text contains multiple runs, the information of each run can be
* obtained from the run index. The level of any particular run indicates the
* direction of the text as well as the nesting level. Left-to-right runs have
* even levels while right-to-left runs have odd levels.
*/
public final class Bidi {
/**
* Constant that indicates the default base level. If there is no strong
* character, then set the paragraph level to 0 (left-to-right).
*/
public static final int DIRECTION_DEFAULT_LEFT_TO_RIGHT = -2;
/**
* Constant that indicates the default base level. If there is no strong
* character, then set the paragraph level to 1 (right-to-left).
*/
public static final int DIRECTION_DEFAULT_RIGHT_TO_LEFT = -1;
/**
* Constant that specifies the default base level as 0 (left-to-right).
*/
public static final int DIRECTION_LEFT_TO_RIGHT = 0;
/**
* Constant that specifies the default base level as 1 (right-to-left).
*/
public static final int DIRECTION_RIGHT_TO_LEFT = 1;
// BEGIN android-removed
// /*
// * Converts the constant from the value specified in the Java spec, to the
// * value required by the ICU implementation.
// */
// private final static int convertDirectionConstant(int javaConst) {
// switch (javaConst) {
// case DIRECTION_DEFAULT_LEFT_TO_RIGHT : return com.ibm.icu.text.Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT;
// case DIRECTION_DEFAULT_RIGHT_TO_LEFT : return com.ibm.icu.text.Bidi.DIRECTION_DEFAULT_RIGHT_TO_LEFT;
// case DIRECTION_LEFT_TO_RIGHT : return com.ibm.icu.text.Bidi.DIRECTION_LEFT_TO_RIGHT;
// case DIRECTION_RIGHT_TO_LEFT : return com.ibm.icu.text.Bidi.DIRECTION_RIGHT_TO_LEFT;
// default : return com.ibm.icu.text.Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT;
// }
// }
//
// /*
// * Use an embedded ICU4J Bidi object to do all the work
// */
// private com.ibm.icu.text.Bidi icuBidi;
// END android-removed
/**
* Creates a {@code Bidi} object from the {@code
* AttributedCharacterIterator} of a paragraph text. The RUN_DIRECTION
* attribute determines the base direction of the bidirectional text. If it
* is not specified explicitly, the algorithm uses
* DIRECTION_DEFAULT_LEFT_TO_RIGHT by default. The BIDI_EMBEDDING attribute
* specifies the level of embedding for each character. Values between -1
* and -62 denote overrides at the level's absolute value, values from 1 to
* 62 indicate embeddings, and the 0 value indicates the level is calculated
* by the algorithm automatically. For the character with no BIDI_EMBEDDING
* attribute or with a improper attribute value, such as a {@code null}
* value, the algorithm treats its embedding level as 0. The NUMERIC_SHAPING
* attribute specifies the instance of NumericShaper used to convert
* European digits to other decimal digits before performing the bidi
* algorithm.
*
* @param paragraph
* the String containing the paragraph text to perform the
* algorithm.
* @throws IllegalArgumentException
* if {@code paragraph} is {@code null}.
* @see java.awt.font.TextAttribute#BIDI_EMBEDDING
* @see java.awt.font.TextAttribute#NUMERIC_SHAPING
* @see java.awt.font.TextAttribute#RUN_DIRECTION
*/
public Bidi(AttributedCharacterIterator paragraph) {
if (paragraph == null) {
// text.14=paragraph is null
throw new IllegalArgumentException(Messages.getString("text.14")); //$NON-NLS-1$
}
// BEGIN android-added
int begin = paragraph.getBeginIndex();
int end = paragraph.getEndIndex();
int length = end - begin;
char text[] = new char[length + 1]; // One more char for
// AttributedCharacterIterator.DONE
if (length != 0) {
text[0] = paragraph.first();
} else {
paragraph.first();
}
// First check the RUN_DIRECTION attribute.
int flags = DIRECTION_DEFAULT_LEFT_TO_RIGHT;
Object direction = paragraph.getAttribute(TextAttribute.RUN_DIRECTION);
if (direction != null && direction instanceof Boolean) {
if (direction.equals(TextAttribute.RUN_DIRECTION_LTR)) {
flags = DIRECTION_LEFT_TO_RIGHT;
} else {
flags = DIRECTION_RIGHT_TO_LEFT;
}
}
// Retrieve the text and gather BIDI_EMBEDDINGS
byte embeddings[] = null;
for (int textLimit = 1, i = 1; i < length; textLimit = paragraph
.getRunLimit(TextAttribute.BIDI_EMBEDDING)
- begin + 1) {
Object embedding = paragraph
.getAttribute(TextAttribute.BIDI_EMBEDDING);
if (embedding != null && embedding instanceof Integer) {
int embLevel = ((Integer) embedding).intValue();
if (embeddings == null) {
embeddings = new byte[length];
}
for (; i < textLimit; i++) {
text[i] = paragraph.next();
embeddings[i - 1] = (byte) embLevel;
}
} else {
for (; i < textLimit; i++) {
text[i] = paragraph.next();
}
}
}
// Apply NumericShaper to the text
Object numericShaper = paragraph
.getAttribute(TextAttribute.NUMERIC_SHAPING);
if (numericShaper != null && numericShaper instanceof NumericShaper) {
((NumericShaper) numericShaper).shape(text, 0, length);
}
long pBidi = createUBiDi(text, 0, embeddings, 0, length, flags);
readBidiInfo(pBidi);
BidiWrapper.ubidi_close(pBidi);
// END android-added
}
/**
* Creates a {@code Bidi} object.
*
* @param text
* the char array of the paragraph text that is processed.
* @param textStart
* the index in {@code text} of the start of the paragraph.
* @param embeddings
* the embedding level array of the paragraph text, specifying
* the embedding level information for each character. Values
* between -1 and -61 denote overrides at the level's absolute
* value, values from 1 to 61 indicate embeddings, and the 0
* value indicates the level is calculated by the algorithm
* automatically.
* @param embStart
* the index in {@code embeddings} of the start of the paragraph.
* @param paragraphLength
* the length of the text to perform the algorithm.
* @param flags
* indicates the base direction of the bidirectional text. It is
* expected that this will be one of the direction constant
* values defined in this class. An unknown value is treated as
* DIRECTION_DEFAULT_LEFT_TO_RIGHT.
* @throws IllegalArgumentException
* if {@code textStart}, {@code embStart}, or {@code
* paragraphLength} is negative; if
* {@code text.length < textStart + paragraphLength} or
* {@code embeddings.length < embStart + paragraphLength}.
* @see #DIRECTION_LEFT_TO_RIGHT
* @see #DIRECTION_RIGHT_TO_LEFT
* @see #DIRECTION_DEFAULT_RIGHT_TO_LEFT
* @see #DIRECTION_DEFAULT_LEFT_TO_RIGHT
*/
public Bidi(char[] text, int textStart, byte[] embeddings, int embStart,
int paragraphLength, int flags) {
if (text == null || text.length - textStart < paragraphLength) {
throw new IllegalArgumentException();
}
if (embeddings != null) {
if (embeddings.length - embStart < paragraphLength) {
throw new IllegalArgumentException();
}
}
if (textStart < 0) {
// text.0D=Negative textStart value {0}
throw new IllegalArgumentException(Messages.getString(
"text.0D", textStart)); //$NON-NLS-1$
}
if (embStart < 0) {
// text.10=Negative embStart value {0}
throw new IllegalArgumentException(Messages.getString(
"text.10", embStart)); //$NON-NLS-1$
}
if (paragraphLength < 0) {
// text.11=Negative paragraph length {0}
throw new IllegalArgumentException(Messages.getString(
"text.11", paragraphLength)); //$NON-NLS-1$
}
// BEGIN android-changed
long pBidi = createUBiDi(text, textStart, embeddings, embStart,
paragraphLength, flags);
readBidiInfo(pBidi);
BidiWrapper.ubidi_close(pBidi);
// END android-changed
}
/**
* Creates a {@code Bidi} object.
*
* @param paragraph
* the string containing the paragraph text to perform the
* algorithm on.
* @param flags
* indicates the base direction of the bidirectional text. It is
* expected that this will be one of the direction constant
* values defined in this class. An unknown value is treated as
* DIRECTION_DEFAULT_LEFT_TO_RIGHT.
* @see #DIRECTION_LEFT_TO_RIGHT
* @see #DIRECTION_RIGHT_TO_LEFT
* @see #DIRECTION_DEFAULT_RIGHT_TO_LEFT
* @see #DIRECTION_DEFAULT_LEFT_TO_RIGHT
*/
public Bidi(String paragraph, int flags) {
this((paragraph == null ? null : paragraph.toCharArray()), 0, null, 0,
(paragraph == null ? 0 : paragraph.length()), flags);
}
// BEGIN android-added
// create the native UBiDi struct, need to be closed with ubidi_close().
private static long createUBiDi(char[] text, int textStart,
byte[] embeddings, int embStart, int paragraphLength, int flags) {
char[] realText = null;
byte[] realEmbeddings = null;
if (text == null || text.length - textStart < paragraphLength) {
throw new IllegalArgumentException();
}
realText = new char[paragraphLength];
System.arraycopy(text, textStart, realText, 0, paragraphLength);
if (embeddings != null) {
if (embeddings.length - embStart < paragraphLength) {
throw new IllegalArgumentException();
}
if (paragraphLength > 0) {
Bidi temp = new Bidi(text, textStart, null, 0, paragraphLength,
flags);
realEmbeddings = new byte[paragraphLength];
System.arraycopy(temp.offsetLevel, 0, realEmbeddings, 0,
paragraphLength);
for (int i = 0; i < paragraphLength; i++) {
byte e = embeddings[i];
if (e < 0) {
realEmbeddings[i] = (byte) (BidiWrapper.UBIDI_LEVEL_OVERRIDE - e);
} else if (e > 0) {
realEmbeddings[i] = e;
} else {
realEmbeddings[i] |= (byte) BidiWrapper.UBIDI_LEVEL_OVERRIDE;
}
}
}
}
if (flags > 1 || flags < -2) {
flags = 0;
}
long bidi = BidiWrapper.ubidi_open();
BidiWrapper.ubidi_setPara(bidi, realText, paragraphLength,
(byte) flags, realEmbeddings);
return bidi;
}
/* private constructor used by createLineBidi() */
private Bidi(long pBidi) {
readBidiInfo(pBidi);
}
// read info from the native UBiDi struct
private void readBidiInfo(long pBidi) {
length = BidiWrapper.ubidi_getLength(pBidi);
offsetLevel = (length == 0) ? null : BidiWrapper.ubidi_getLevels(pBidi);
baseLevel = BidiWrapper.ubidi_getParaLevel(pBidi);
int runCount = BidiWrapper.ubidi_countRuns(pBidi);
if (runCount == 0) {
unidirectional = true;
runs = null;
} else if (runCount < 0) {
runs = null;
} else {
runs = BidiWrapper.ubidi_getRuns(pBidi);
// Simplified case for one run which has the base level
if (runCount == 1 && runs[0].getLevel() == baseLevel) {
unidirectional = true;
runs = null;
}
}
direction = BidiWrapper.ubidi_getDirection(pBidi);
}
private int baseLevel;
private int length;
private byte[] offsetLevel;
private BidiRun[] runs;
private int direction;
private boolean unidirectional;
// END android-added
/**
* Returns whether the base level is from left to right.
*
* @return true if the base level is from left to right.
*/
public boolean baseIsLeftToRight() {
// BEGIN android-changed
return baseLevel % 2 == 0 ? true : false;
// END android-changed
}
/**
* Creates a new {@code Bidi} object containing the information of one line
* from this object.
*
* @param lineStart
* the start offset of the line.
* @param lineLimit
* the limit of the line.
* @return the new line Bidi object. In this new object, the indices will
* range from 0 to (limit - start - 1).
* @throws IllegalArgumentException
* if {@code lineStart < 0}, {@code lineLimit < 0}, {@code
* lineStart > lineLimit} or if {@code lineStart} is greater
* than the length of this object's paragraph text.
*/
public Bidi createLineBidi(int lineStart, int lineLimit) {
// BEGIN android-removed
// int length = icuBidi.getLength();
// END android-removed
if (lineStart < 0 || lineLimit < 0 || lineLimit > length
|| lineStart > lineLimit) {
// text.12=Invalid ranges (start={0}, limit={1}, length={2})
throw new IllegalArgumentException(Messages.getString(
"text.12", new Object[] { lineStart, lineLimit, length })); //$NON-NLS-1$
}
// BEGIN android-changed
char[] text = new char[this.length];
Arrays.fill(text, 'a');
byte[] embeddings = new byte[this.length];
for (int i = 0; i < embeddings.length; i++) {
embeddings[i] = (byte) -this.offsetLevel[i];
}
int dir = this.baseIsLeftToRight() ? Bidi.DIRECTION_LEFT_TO_RIGHT
: Bidi.DIRECTION_RIGHT_TO_LEFT;
long parent = createUBiDi(text, 0, embeddings, 0, this.length, dir);
long line = BidiWrapper.ubidi_setLine(parent, lineStart, lineLimit);
Bidi result = new Bidi(line);
BidiWrapper.ubidi_close(line);
BidiWrapper.ubidi_close(parent);
return result;
// END android-changed
}
/**
* Returns the base level.
*
* @return the base level.
*/
public int getBaseLevel() {
// BEGIN android-changed
return baseLevel;
// END android-changed
}
/**
* Returns the length of the text in the {@code Bidi} object.
*
* @return the length.
*/
public int getLength() {
// BEGIN android-changed
return length;
// END android-changed
}
/**
* Returns the level of a specified character.
*
* @param offset
* the offset of the character.
* @return the level.
*/
public int getLevelAt(int offset) {
// BEGIN android-changed
try {
return offsetLevel[offset] & ~BidiWrapper.UBIDI_LEVEL_OVERRIDE;
} catch (RuntimeException e) {
return baseLevel;
}
// END android-changed
}
/**
* Returns the number of runs in the bidirectional text.
*
* @return the number of runs, at least 1.
*/
public int getRunCount() {
// BEGIN android-changed
return unidirectional ? 1 : runs.length;
// END android-changed
}
/**
* Returns the level of the specified run.
*
* @param run
* the index of the run.
* @return the level of the run.
*/
public int getRunLevel(int run) {
// BEGIN android-changed
return unidirectional ? baseLevel : runs[run].getLevel();
// END android-changed
}
/**
* Returns the limit offset of the specified run.
*
* @param run
* the index of the run.
* @return the limit offset of the run.
*/
public int getRunLimit(int run) {
// BEGIN android-changed
return unidirectional ? length : runs[run].getLimit();
// END android-changed
}
/**
* Returns the start offset of the specified run.
*
* @param run
* the index of the run.
* @return the start offset of the run.
*/
public int getRunStart(int run) {
// BEGIN android-changed
return unidirectional ? 0 : runs[run].getStart();
// END android-changed
}
/**
* Indicates whether the text is from left to right, that is, both the base
* direction and the text direction is from left to right.
*
* @return {@code true} if the text is from left to right; {@code false}
* otherwise.
*/
public boolean isLeftToRight() {
// BEGIN android-changed
return direction == BidiWrapper.UBiDiDirection_UBIDI_LTR;
// END android-changed
}
/**
* Indicates whether the text direction is mixed.
*
* @return {@code true} if the text direction is mixed; {@code false}
* otherwise.
*/
public boolean isMixed() {
// BEGIN android-changed
return direction == BidiWrapper.UBiDiDirection_UBIDI_MIXED;
// END android-changed
}
/**
* Indicates whether the text is from right to left, that is, both the base
* direction and the text direction is from right to left.
*
* @return {@code true} if the text is from right to left; {@code false}
* otherwise.
*/
public boolean isRightToLeft() {
// BEGIN android-changed
return direction == BidiWrapper.UBiDiDirection_UBIDI_RTL;
// END android-changed
}
/**
* Reorders a range of objects according to their specified levels. This is
* a convenience function that does not use a {@code Bidi} object. The range
* of objects at {@code index} from {@code objectStart} to {@code
* objectStart + count} will be reordered according to the range of levels
* at {@code index} from {@code levelStart} to {@code levelStart + count}.
*
* @param levels
* the level array, which is already determined.
* @param levelStart
* the start offset of the range of the levels.
* @param objects
* the object array to reorder.
* @param objectStart
* the start offset of the range of objects.
* @param count
* the count of the range of objects to reorder.
* @throws IllegalArgumentException
* if {@code count}, {@code levelStart} or {@code objectStart}
* is negative; if {@code count > levels.length - levelStart} or
* if {@code count > objects.length - objectStart}.
*/
public static void reorderVisually(byte[] levels, int levelStart,
Object[] objects, int objectStart, int count) {
if (count < 0 || levelStart < 0 || objectStart < 0
|| count > levels.length - levelStart
|| count > objects.length - objectStart) {
// text.13=Invalid ranges (levels={0}, levelStart={1}, objects={2},
// objectStart={3}, count={4})
throw new IllegalArgumentException(Messages.getString("text.13", //$NON-NLS-1$
new Object[] { levels.length, levelStart, objects.length,
objectStart, count }));
}
// BEGIN android-changed
byte[] realLevels = new byte[count];
System.arraycopy(levels, levelStart, realLevels, 0, count);
int[] indices = BidiWrapper.ubidi_reorderVisual(realLevels, count);
LinkedList<Object> result = new LinkedList<Object>();
for (int i = 0; i < count; i++) {
result.addLast(objects[objectStart + indices[i]]);
}
System.arraycopy(result.toArray(), 0, objects, objectStart, count);
// END android-changed
}
/**
* Indicates whether a range of characters of a text requires a {@code Bidi}
* object to display properly.
*
* @param text
* the char array of the text.
* @param start
* the start offset of the range of characters.
* @param limit
* the limit offset of the range of characters.
* @return {@code true} if the range of characters requires a {@code Bidi}
* object; {@code false} otherwise.
* @throws IllegalArgumentException
* if {@code start} or {@code limit} is negative; {@code start >
* limit} or {@code limit} is greater than the length of this
* object's paragraph text.
*/
public static boolean requiresBidi(char[] text, int start, int limit) {
//int length = text.length;
if (limit < 0 || start < 0 || start > limit || limit > text.length) {
throw new IllegalArgumentException();
}
// BEGIN android-changed
Bidi bidi = new Bidi(text, start, null, 0, limit - start, 0);
return !bidi.isLeftToRight();
// END android-changed
}
/**
* Returns the internal message of the {@code Bidi} object, used in
* debugging.
*
* @return a string containing the internal message.
*/
@Override
public String toString() {
// BEGIN android-changed
return super.toString()
+ "[direction: " + direction + " baselevel: " + baseLevel //$NON-NLS-1$ //$NON-NLS-2$
+ " length: " + length + " runs: " + (unidirectional ? "null" : runs.toString()) + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
// END android-changed
}
}