Hi Max,

> DISCLAIMER: These comments are to be seen as purely "academic", and may
> be complete overkill. For practical purposes, your code is just fine.

No, its ok, I like code reviews.

> - value is a very generic name, and could be reconsidered. What does the
> "value" actually specify? Looking at the detail, it is the int
> representation of the tag in little-endian. So I'd propose
> intRepresentation instead.

You are right, value is a bit to generic. The representation is actually
big-endian, the first byte of the array is the highest byte. So I should
really put this information in a comment. I'm even not sure why I've
chosen such a compact and difficult to understand representation. A
String would be better.

> - in your constructor, you use value to build up the intRepresentation.
> In this case, I'd use something like "intValue"

Here I would say that repeating the type in the variable name is not
needed. So the question would be why repeating "int" in
"intRepresentation"? Than one could say that the field should be really
named "compactBigEndianIntegerRepresentation". But than this whole
concept of a compact big-endian integer representation of a String with
length of four and reduced ASCII charset should be really go into its
own class.

> - you have a static method  valueOf(String) and a constructor (byte[]).
> Why two different ways of initializing the class?

The valueOf(String) is the only public constructor. Its used all over
the font subsystem to create tags if needed. The package private
constructor is only used in the OpenTypeDataInputImpl. So here I have
the reading code of the data representation in the OpenType file (byte
array of length four) at the wrong place. It really needs to be moved
into the OpenTypeDataInputImpl. 

> - The constructor should be made private. If you really need to access
> the (byte[]) from within the package, you may provide a static public
> method for access.

Yes this byte[] constructor is a bit odd.

> - This class could be optimized using the flyweight pattern (e.g.
> caching the created objects)

Yes you are right, it could. But I really like to have ConcurrentHashMap
for such a task. So maybe I should wait until we switch to Java 1.5 or
can you recommend the ConcurrenthashMap from the backports JAR?

> - equals would be more readable if you rename tag to otherTag, and use
> this.value == otherTag.value

Yes, please blame Intellij Idea for that.

> - checkByte also uses "value". In this case, you mean "byteValue" or
> "charValue".

You are right!

> - why go with "toChars" creating an array and then using it?
> StringBuilder may be the easier solution.
> 
> - in the "alluppercase" and "alllowercase" methods: You may consider
> using Character.isLetter rather than explicitly checking for space and
> numbers. Some characters, such as @ (although probably not used) would
> otherwise create bugs.

The problem is, that a digit is also considered as lowercase. In fact I
realized that this method should be named "containsNoUpperCaseLetters".
I also changed the implementation to:

    if (Character.isLetter(ch) && Character.isUpperCase(ch)) {
        return false;
    }

> > Another example is the method getEntriesInOffsetOrder() in the attached
> > file OffsetTable.java. It is just a getter of entries but it is named
> > different.
> 
> getEntriesInOffsetOrder returns a sorted version of the entries. So why
> not call the variable sortedEntries?

Because it is not sorted before calling Collections.sort(). If you read

    List sortedEntries = getEntries();

you would expect, that getEntries() will return alright sorted entries.
The problem is, that Collections.sort() uses the "output parameter"
anti-pattern.

> Other notes:
> - getEntries does not return the entries attribute. This means you are
> confusing internal and external representation. getEntrieValues() could
> be a better name.

No entries is really a simple collection of enties (look at the
constructor). The Map is really a mapping from tag to entry. So I should
name it tagToEntryMap. Ahh and than the problem with the hidden variable
is also solved. Good point :-)

> - since the entries are re-ordered anyways when adding to the map, why
> not use a SortedMap (e.g. TreeMap instead)? Then one "getEntries" method
> would suffice.

Uhh. You spottet a bug. I need a LinkedHashMap here. Thanks! I really
like to have the entries in original order and in offset order.

> - you have some default visibility methods and classes, would should be
> reconsidered.

What is wrong with package local visibility? I find it very useful. In
fact, I think, its the most useful visibility right after private.

> > I think this rule ist mostly helpful in order to think about variable
> > names. But I also think that here are a few cases where violating this
> > rule makes sense. So maybe the rule ist just not smart enough to detect
> > the remaining special cases.
> 
> If you are really sure you can always temporary disable CHECKSTLYE with
> 
> // CHECKSTYLE:OFF
> violating code
> // CHECKSTYLE:ON

I that allowed? :-)

> > Thats the same as with the "Javadoc on public things rule". If there
> > isn't anything to say about a public thing which will say more than the
> > name itself, than I prefer no comment at all. But how should Checkstyle
> > detect such cases? 
> 
> In most cases there is more information to be transported. For example
> the "getEntriesInOffsetOrder" method. This may be clear to you because
> you have written the code, but I first had to think for a while before I
> realized what the "offsetOrder" is. It was easier for me since I saw the
> source code. But would I use your class, it would not get it immediately.

Yes this class is just not finished yet. The offset order needs
explanation. 

I thank you for your code review. At the end I have now a much cleaner
Tag class. I attached all relevant files to this mail.

Best Regards
Alex
/*
 * 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$ */

package org.apache.fop.fonts.opentype.common.io;

import junit.framework.TestCase;

/**
 * Tests the {...@link Tag} class.
 */
public class TagTest extends TestCase {

    public void testValueOf() {
        assertEquals("ttcf", Tag.valueOf("ttcf").toString());
    }

    public void testValueOfToShort() {
        try {
            Tag.valueOf("123");
            fail("expected IllegalArgumentException");
        } catch (IllegalArgumentException e) {
            assertEquals("Invalid name length. The required length is 4 but was: 3. "
                    + "The name was '123'.", e.getMessage());
        }
    }

    public void testValueOfToLong() {
        try {
            Tag.valueOf("12345");
            fail("expected IllegalArgumentException");
        } catch (IllegalArgumentException e) {
            assertEquals("Invalid name length. The required length is 4 but was: 5. "
                    + "The name was '12345'.", e.getMessage());
        }
    }

    public void testValueOfIllegalCharacter31() {
        try {
            Tag.valueOf("\u001Faaa");
            fail("expected IllegalArgumentException");
        } catch (IllegalArgumentException e) {
            assertEquals("The name '\u001Faaa' contains illegal characters.", e.getMessage());
        }
    }

    public void testValueOfIllegalCharacter127() {
        try {
            Tag.valueOf("\u007Faaa");
            fail("expected IllegalArgumentException");
        } catch (IllegalArgumentException e) {
            assertEquals("The name '\u007Faaa' contains illegal characters.", e.getMessage());
        }
    }

    public void testAllUpperCase() {
        assertTrue(Tag.valueOf("DFLT").containsNoLowerCaseLetters());
        assertTrue(Tag.valueOf("OS/2").containsNoLowerCaseLetters());
        assertTrue(Tag.valueOf("AAA1").containsNoLowerCaseLetters());
        assertTrue(Tag.valueOf("AAA ").containsNoLowerCaseLetters());
        assertTrue(Tag.valueOf(" AAA").containsNoLowerCaseLetters());

        assertFalse(Tag.valueOf("cyrl").containsNoLowerCaseLetters());
        assertFalse(Tag.valueOf("gjr2").containsNoLowerCaseLetters());
        assertFalse(Tag.valueOf("aaa ").containsNoLowerCaseLetters());
        assertFalse(Tag.valueOf(" aaa").containsNoLowerCaseLetters());
    }

    public void testAllLowerCase() {
        assertTrue(Tag.valueOf("cyrl").containsNoUpperCaseLetters());
        assertTrue(Tag.valueOf("gjr2").containsNoUpperCaseLetters());
        assertTrue(Tag.valueOf("aaa ").containsNoUpperCaseLetters());
        assertTrue(Tag.valueOf(" aaa").containsNoUpperCaseLetters());

        assertFalse(Tag.valueOf("DFLT").containsNoUpperCaseLetters());
        assertFalse(Tag.valueOf("OS/2").containsNoUpperCaseLetters());
        assertFalse(Tag.valueOf("AAA1").containsNoUpperCaseLetters());
        assertFalse(Tag.valueOf("AAA ").containsNoUpperCaseLetters());
        assertFalse(Tag.valueOf(" AAA").containsNoUpperCaseLetters());
    }
    
    public void testEquals() {
        Tag tag = Tag.valueOf("test");
        assertEquals(tag, tag);
        assertEquals(tag, Tag.valueOf("test"));
        assertFalse(tag.equals(Tag.valueOf("TEST")));
        assertFalse(tag.equals(null));
        assertFalse(tag.equals("test"));
    }

    public void testHashCode() {
        Tag flags = Tag.valueOf("test");
        assertEquals(flags.hashCode(), flags.hashCode());
        assertEquals(flags.hashCode(), Tag.valueOf("test").hashCode());
    }
}
/*
 * 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$ */

package org.apache.fop.fonts.opentype.common.io;

import java.io.EOFException;
import java.io.IOException;
import java.util.Date;

/**
 * This interface provides methods for reading OpenType data types from a binary stream.
 * <p/>
 * It is similar to {...@link java.io.DataInput} which is an interface for reading Java data types.
 *
 * @see java.io.DataInput
 */
public interface OpenTypeDataInput {

    /**
     * Skips over the given number of bytes.
     *
     * @param numberOfBytes the number of bytes to skip over
     * @throws IOException
     */
    void skip(int numberOfBytes) throws IOException;

    void readFully(byte[] bytes) throws IOException;

    void readFully(byte[] bytes, int off, int len) throws IOException;

    /**
     * Reads a <code>BYTE</code> which is a 8-bit unsigned integer.
     * <p/>
     * Reads one input byte, zero-extends it to type <code>int</code>, and returns the result, which
     * is therefore in the range <code>0</code> through <code>255</code> inclusive.
     *
     * @return the next byte of this input stream, interpreted as an 8-bit unsigned integer.
     * @throws EOFException if this stream reaches the end before reading all the bytes.
     * @throws IOException  if an I/O error occurs.
     */
    int readByte() throws IOException;

    /**
     * Reads a <code>CHAR</code> which is a 8-bit signed integer.
     * <p/>
     * Reads one input byte, and returns the result, which is therefore in the range
     * <code>-128</code> through <code>127</code> inclusive.
     *
     * @return the next byte of this input stream, interpreted as an 8-bit signed integer.
     * @throws EOFException if this stream reaches the end before reading all the bytes.
     * @throws IOException  if an I/O error occurs.
     */
    byte readChar() throws IOException;

    /**
     * Reads a <code>USHORT</code> which is a 16-bit unsigned integer.
     * <p/>
     * Reads two input bytes and returns a <code>int</code> value in the range <code>0</code>
     * through <code>65535</code>. Let <code>a</code> be the first byte read and <code>b</code> be
     * the second byte. The value returned is: <code>(a << 8) | (b & 0xff)</code>
     *
     * @return the next two bytes of this input stream, interpreted as an 16-bit unsigned integer.
     * @throws EOFException if this input stream reaches the end before reading two bytes.
     * @throws IOException  if an I/O error occurs.
     */
    int readUnsignedShort() throws IOException;

    /**
     * Reads a <code>SHORT</code> which is a 16-bit signed integer.
     * <p/>
     * Reads two input bytes and returns a <code>int</code> value in the range <code>-32768</code>
     * through <code>32767</code>. Let <code>a</code> be the first byte read and <code>b</code> be
     * the second byte. The value returned is: <code>(a << 8) | (b & 0xff)</code>
     *
     * @return the next two bytes of this input stream, interpreted as an 16-bit signed integer.
     * @throws EOFException if this input stream reaches the end before reading two bytes.
     * @throws IOException  if an I/O error occurs.
     */
    short readShort() throws IOException;

    /**
     * Reads a <code>UINT24</code> which is a 24-bit unsigned integer.
     * <p/>
     * Reads three input bytes and returns a <code>int</code> value in the range <code>0</code>
     * through <code>2^24-1</code>. Let <code>a</code> be the first byte read, <code>b</code> be the
     * second byte read and <code>c</code> be the third byte read. The value returned is: <code>(a
     * << 16) | (b << 8) | (c & 0xff)</code>
     *
     * @return the next three bytes of this input stream, interpreted as an 24-bit unsigned
     *         integer.
     * @throws EOFException if this input stream reaches the end before reading three bytes.
     * @throws IOException  if an I/O error occurs.
     */
    int readUnsignedInteger24() throws IOException;

    /**
     * Reads a <code>ULONG</code> which is a 32-bit unsigned integer.
     * <p/>
     * Reads four input bytes and returns a <code>int</code> value in the range <code>0</code>
     * through <code>2^32-1</code>. Let <code>a</code> be the first byte read, <code>b</code> be the
     * second byte read, <code>c</code> be the third byte read and <code>d</code> the fourth byte
     * read. The value returned is: <code>(a << 24) | (b << 16) | (c << 8) | (d & 0xff)</code>
     *
     * @return the next three bytes of this input stream, interpreted as an 32-bit unsigned
     *         integer.
     * @throws EOFException if this input stream reaches the end before reading four bytes.
     * @throws IOException  if an I/O error occurs.
     */
    long readUnsignedLong() throws IOException;

    /**
     * Reads a <code>LONG</code> which is a 32-bit signed integer.
     * <p/>
     * Reads four input bytes and returns a <code>int</code> value in the range <code>0</code>
     * through <code>2^32-1</code>. Let <code>a</code> be the first byte read, <code>b</code> be the
     * second byte read, <code>c</code> be the third byte read and <code>d</code> the fourth byte
     * read. The value returned is: <code>(a << 24) | (b << 16) | (c << 8) | (d & 0xff)</code>
     *
     * @return the next three bytes of this input stream, interpreted as an 32-bit signed integer.
     * @throws EOFException if this input stream reaches the end before reading four bytes.
     * @throws IOException  if an I/O error occurs.
     */
    int readLong() throws IOException;

    /**
     * Reads a <code>Fixed</code> which is a 32-bit signed fixed-point number (16.16).
     * <p/>
     * Reads four input bytes and returns a <code>int</code> value in the range <code>0</code>
     * through <code>2^32-1</code>. Let <code>a</code> be the first byte read, <code>b</code> be the
     * second byte read, <code>c</code> be the third byte read and <code>d</code> the fourth byte
     * read. The value returned is: <code>(a << 24) | (b << 16) | (c << 8) | (d & 0xff)</code>
     *
     * @return the next three bytes of this input stream, interpreted as an 32-bit signed integer.
     * @throws EOFException if this input stream reaches the end before reading four bytes.
     * @throws IOException  if an I/O error occurs.
     */
    Fixed readFixed() throws IOException;

    VersionNumber readFixedVersionNumber() throws IOException;

    Date readLongDateTime() throws IOException;

    /**
     * Reads a <code>Tag</code> which is a 4-byte type.
     *
     * @return the next four bytes of this input stream, interpreted as a <code>Tag</code>.
     * @throws OpenTypeDataFormatException if one of the bytes is invalid for a <code>Tag</code>
     * @throws EOFException                if this input stream reaches the end before reading four
     *                                     bytes.
     * @throws IOException                 if an I/O error occurs.
     */
    Tag readTag() throws IOException;

    int readOffset() throws IOException;

    void setUnitsPerEm(int unitsPerEm);

    Rectangle readBox() throws IOException;

    Point readPoint() throws IOException;

    FLength readFLength() throws IOException;

    FLength readUnsignedFLength() throws IOException;
}
/*
 * 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.
 */

package org.apache.fop.fonts.opentype.common.io;

import java.io.DataInput;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
import java.util.regex.Pattern;

/**
 *
 */
public class OpenTypeDataInputImpl {

    private static final int TWO_BYTE_SHIFT = 16;
    private static final int ONE_BYTE_SHIFT = 8;

    private static final long EPOCH;

    static {
        Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        calendar.set(Calendar.YEAR, 1904);
        calendar.set(Calendar.MONTH, Calendar.JANUARY);
        calendar.set(Calendar.DAY_OF_MONTH, 1);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        EPOCH = calendar.getTimeInMillis();
    }

    protected final DataInput in;
    private final byte[] byteBuffer = new byte[8];
    private int unitsPerEm = -1;
    static final int MIN_BYTE_VALUE = 0x20;
    static final int MAX_BYTE_VALUE = 0x7E;

    public OpenTypeDataInputImpl(DataInput in) {
        this.in = in;
    }

    public void setUnitsPerEm(int unitsPerEm) {
        this.unitsPerEm = unitsPerEm;
    }

    public void readFully(byte[] bytes) throws IOException {
        in.readFully(bytes);
    }

    public void readFully(byte[] bytes, int off, int len) throws IOException {
        in.readFully(bytes, off, len);
    }

    public void skip(int numberOfBytes) throws IOException {
        int numberOfBytesSkipped = in.skipBytes(numberOfBytes);
        if (numberOfBytesSkipped != numberOfBytes) {
            throw new IOException("Wasn't able to skip the specified number of bytes.");
        }
    }

    public final int readByte() throws IOException {
        return in.readUnsignedByte();
    }

    public final byte readChar() throws IOException {
        return in.readByte();
    }

    public final int readUnsignedShort() throws IOException {
        return in.readUnsignedShort();
    }

    public final short readShort() throws IOException {
        return in.readShort();
    }

    public final int readUnsignedInteger24() throws IOException {
        int high = in.readUnsignedShort();
        int low = in.readUnsignedByte();
        return (high << ONE_BYTE_SHIFT) + low;
    }

    public final long readUnsignedLong() throws IOException {
        int high = in.readUnsignedShort();
        int low = in.readUnsignedShort();
        return (((long) high) << TWO_BYTE_SHIFT) + low;
    }

    public final int readLong() throws IOException {
        return in.readInt();
    }

    public final Fixed readFixed() throws IOException {
        return new Fixed(readShort(), readUnsignedShort());
    }

    public final VersionNumber readFixedVersionNumber() throws IOException {
        int majorHigh = in.readUnsignedByte();
        int majorLow = in.readUnsignedByte();
        int minorHigh = in.readUnsignedByte();
        int minorLow = in.readUnsignedByte();
        return new FixedVersionNumber(createFixedVersionNumberPart(majorHigh, majorLow),
                createFixedVersionNumberPart(minorHigh, minorLow));
    }

    private int createFixedVersionNumberPart(int majorHigh, int majorLow)
            throws OpenTypeDataFormatException {
        return checkDecimal(majorHigh >> 4) * 1000
                + checkDecimal(majorHigh & 0x0F) * 100
                + checkDecimal(majorLow >> 4) * 10
                + checkDecimal(majorLow & 0x0F);
    }

    private int checkDecimal(int digit) throws OpenTypeDataFormatException {
        assert digit >= 0;
        if (digit >= 10) {
            throw new OpenTypeDataFormatException("digit >= 10; was " + digit);
        }
        return digit;
    }

    public final Date readLongDateTime() throws IOException {
        long secondsSinceEpoch = in.readLong();
        return new Date(EPOCH + secondsSinceEpoch * 1000);
    }

    public final Tag readTag() throws IOException {
        String name = readAsciiString(4);
        if (!Tag.isValidTagName(name)) {
            throw new OpenTypeDataFormatException("The tag name '" + name
                    + "' contains illegal characters.");
        }
        return Tag.valueOf(name);
    }

    private String readAsciiString(int length) throws IOException {
        if (length > byteBuffer.length) {
            throw new IllegalArgumentException("buffer to short");
        }

        in.readFully(byteBuffer, 0, length);

        try {
            return new String(byteBuffer, 0, length, "ASCII");
        } catch (UnsupportedEncodingException e) {
            throw new OpenTypeDataFormatException("ASCII encoding is unsupported on this "
                    + "platform: " + e.getMessage());
        }
    }

    public final int readOffset() throws IOException {
        return readUnsignedShort();
    }

    public final Rectangle readBox() throws IOException {
        return new Rectangle(readPoint(), readPoint());
    }

    public final Point readPoint() throws IOException {
        return new Point(readFLength(), readFLength());
    }

    public final FLength readFLength() throws IOException {
        return new FLength(readShort(), getUnitsPerEm());
    }

    public final FLength readUnsignedFLength() throws IOException {
        return new FLength(readUnsignedShort(), getUnitsPerEm());
    }

    private int getUnitsPerEm() {
        if (unitsPerEm < 0) {
            throw new IllegalStateException("unitsPerEm not set");
        }
        return unitsPerEm;
    }
}
/*
 * 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 */

package org.apache.fop.fonts.opentype.file;

import org.apache.fop.fonts.opentype.common.io.Tag;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 *
 */
final class OffsetTable {

    private final Map tagToEntryMap;

    OffsetTable(Entry[] entries) {
        this.tagToEntryMap = new LinkedHashMap(entries.length);
        for (int i = 0; i < entries.length; i++) {
            Entry entry = entries[i];
            this.tagToEntryMap.put(entry.getTag(), entry);
        }
    }

    public List getEntries() {
        return new ArrayList(tagToEntryMap.values());
    }

    public List getEntriesInOffsetOrder() {
        List entries = getEntries();
        Collections.sort(entries, ENTRY_OFFSET_COMPARATOR);
        return entries;
    }

    /**
     * Determines whether this {...@code OffsetTable} contains an entry for the given tag.
     *
     * @param tag the tag to query
     * @return {...@code true} if this {...@code OffsetTable} contains an entry for the given tag: {...@code
     *         false} otherwise
     */
    public boolean containsEntry(Tag tag) {
        return tagToEntryMap.containsKey(tag);
    }

    /**
     * Returns the entry with the given tag or {...@code null} if there is no such entry.
     *
     * @param tag the tag to query
     * @return the entry with the given tag or {...@code null} if there is no such entry.
     */
    public Entry getEntry(Tag tag) {
        return (Entry) tagToEntryMap.get(tag);
    }

    private static final Comparator ENTRY_OFFSET_COMPARATOR = new Comparator() {
        public int compare(Object o1, Object o2) {
            long offset1 = ((Entry) o1).offset;
            long offset2 = ((Entry) o2).offset;
            return offset1 < offset2 ? -1 : offset1 > offset2 ? 1 : 0;
        }
    };

    static class Entry {

        private final Tag tag;
        private final long checksum;
        private final long offset;
        private final long length;

        Entry(Tag tag, long checksum, long offset, long length) {
            this.tag = tag;
            this.checksum = checksum;
            this.offset = offset;
            this.length = length;
        }

        public Tag getTag() {
            return tag;
        }

        public long getChecksum() {
            return checksum;
        }

        public long getOffset() {
            return offset;
        }

        public long getLength() {
            return length;
        }

        public String toString() {
            return "OffsetTable.Entry["
                    + "tag = " + tag
                    + ", checksum = " + checksum
                    + ", offset = " + offset
                    + ", length = " + length
                    + "]";
        }
    }
}
/*
 * 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 */

package org.apache.fop.fonts.opentype.common.io;

import java.util.regex.Pattern;

/**
 * This class represents a tag, a basic data type of OpenType, used to identify a table, script,
 * language system, feature, or baseline.
 * <p/>
 * All tags consist of a four character long string which is called the name of the tag. Tag names
 * with less than four characters are allowed if followed by the necessary trailing spaces. All tag
 * names defined within a font (e.g., table names, feature tags, language tags) must be built from
 * printing characters represented by ASCII values 32-126.
 */
public final class Tag {

    private static final int REQUIRED_NAME_LENGTH = 4;
    private static final Pattern NAME_PATTERN = Pattern.compile("\\p{Print}{4}");
    private static final Pattern UPPERCASE_LETTER_PATTERN = Pattern.compile("\\p{Upper}");
    private static final Pattern LOWERCASE_LETTER_PATTERN = Pattern.compile("\\p{Lower}");

    private final String name;

    /**
     * Creates a tag with the given name.
     * <p/>
     * Tag names have to consist of exactly four characters. Moreover the character range is
     * restricted to the printing characters represented by the ASCII values between including 32
     * and 126.
     *
     * @param name the name of the tag to create.
     * @return a tag with the given name.
     * @throws IllegalArgumentException if the length of the name isn't four or not all characters
     *                                  are in the range of printing characters represented by ASCII
     *                                  values 32-126.
     */
    public static Tag valueOf(String name) {
        return new Tag(name);
    }

    private Tag(String name) {
        if (!isValidTagName(name)) {
            throw new IllegalArgumentException(buildErrorMessage(name));
        }
        this.name = name;
    }

    private String buildErrorMessage(String invalidName) {
        int length = invalidName.length();

        if (length != REQUIRED_NAME_LENGTH) {
            return "Invalid name length. The required length is " + REQUIRED_NAME_LENGTH
                    + " but was: " + length + ". The name was '" + invalidName + "'.";
        } else {
            return "The name '" + invalidName + "' contains illegal characters.";
        }
    }

    /**
     * Determines whether the given name is a valid tag name.
     *
     * @param name the name to test
     * @return <code>true</code> if the given name is a valid tag name; <code>false</code>
     *         otherwise.
     */
    public static boolean isValidTagName(String name) {
        return NAME_PATTERN.matcher(name).matches();
    }

    /**
     * Determines whether the name of this tag contains no lowercase letters.
     *
     * @return <code>true</code> if the name of this tag contains no lowercase letters;
     *         <code>false</code> otherwise.
     */
    public boolean containsNoLowerCaseLetters() {
        return !containsLowerCaseLetters();
    }

    private boolean containsLowerCaseLetters() {
        return LOWERCASE_LETTER_PATTERN.matcher(name).find();
    }

    /**
     * Determines whether the name of this tag contains no uppercase letters.
     *
     * @return <code>true</code> if the name of this tag contains no uppercase letters;
     *         <code>false</code> otherwise.
     */
    public boolean containsNoUpperCaseLetters() {
        return !containsUpperCaseLetters();
    }

    private boolean containsUpperCaseLetters() {
        return UPPERCASE_LETTER_PATTERN.matcher(name).find();
    }

    /**
     * Determines whether this tag equals the given object.
     * <p/>
     * The class of the object has to be <code>Tag</code> and the name of the tag has to be equal to
     * this tags name. The names are compared case-sesnsitive.
     *
     * @param obj the object with which to compare.
     * @return <code>true</code> if the above made rules match; <code>false</code> otherwise.
     */
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }

        Tag otherTag = (Tag) obj;

        return this.name.equals(otherTag.name);
    }

    /**
     * {...@inheritdoc}
     */
    public int hashCode() {
        return name.hashCode();
    }

    /**
     * Returns exactely and only the name of this tag.
     *
     * @return exactely and only the name of this tag.
     */
    public String toString() {
        return name;
    }
}

Attachment: signature.asc
Description: This is a digitally signed message part

Reply via email to