    public static String normalize(String pidString) 
            throws MalformedPIDException {
        if (pidString == null) {
            throw new MalformedPIDException("PID is null.");
        }
        if (pidString.length() > MAX_LENGTH) {
            throw new MalformedPIDException("PID length exceeds " 
                    + MAX_LENGTH + ".");
        }
        StringBuffer out = new StringBuffer();
        boolean inObjectID = false;
        for (int i = 0; i < pidString.length(); i++) {
            char c = pidString.charAt(i);
            if (!inObjectID) {
                if (c == ':') {
                    out.append(':');
                    inObjectID = true;
                } else if (c == '%') {
                    if (pidString.length() >= i + 3) {
                        i++;
                        if (pidString.charAt(i) == '3') {
                            i++;
                            c = pidString.charAt(i);
                            if ( c == 'a' || c == 'A' ) {
                                out.append(":");
                                inObjectID = true;
                            } else {
                                throw new MalformedPIDException("Error in PID after first '%': expected '3a' or '3A', but saw '3" + c + "'.");
                            }
                        } else {
                            throw new MalformedPIDException("Error in PID after first '%': expected '3a' or '3A', but saw '" + pidString.substring(i, i+2) + "'.");
                        }
                    } else {
                        throw new MalformedPIDException("Error in PID after first '%': expected '3a' or '3A', but saw '" + pidString.substring(i+1) + "'.");
                    }
                } else if (isAlphaNum(c) || c == '-' || c == '.') {
                    out.append(c);
                } else {
                    throw new MalformedPIDException("PID namespace-id cannot contain '" + c + "' character.");
                }
            } else if (isAlphaNum(c) || c == '-' || c == '.' || c == '~' || c == '_' ) {
                out.append(c);
            } else if (c == '%') {
                if (pidString.length() >= i + 3) {
                    char h1 = getNormalizedHexChar(pidString.charAt(++i));
                    char h2 = getNormalizedHexChar(pidString.charAt(++i));
                    out.append("%" + h1 + h2);
                } else {
                    throw new MalformedPIDException("PID object-id ended early: need at least 2 chars after '%'.");
                }
            } else {
                throw new MalformedPIDException("PID object-id cannot contain '" + c + "' character.");
            }
        }
        if (!inObjectID) throw new MalformedPIDException("PID delimiter (:) is missing.");
        String outString = out.toString();
        if (outString.startsWith(":")) throw new MalformedPIDException("PID namespace-id cannot be empty.");
        if (outString.length() < 3) throw new MalformedPIDException("PID object-id cannot be empty.");
        return out.toString();
    }

    public static String normalize(String pidString) 
            throws MalformedPIDException {
        if (pidString == null) {
            throw new MalformedPIDException("PID is null.");
        }
        if (pidString.length() > MAX_LENGTH) {
            throw new MalformedPIDException("PID length exceeds " 
                    + MAX_LENGTH + ".");
        }
        StringBuffer out = new StringBuffer();
        boolean inObjectID = false;
        for (int i = 0; i < pidString.length(); i++) {
            char c = pidString.charAt(i);
            if (!inObjectID) {
                if (c == ':') {
                    out.append(':');
                    inObjectID = true;
                } else if (c == '%') {
                    if (pidString.length() >= i + 3) {
                        i++;
                        if (pidString.charAt(i) == '3') {
                            i++;
                            c = pidString.charAt(i);
                            if ( c == 'a' || c == 'A' ) {
                                out.append(":");
                                inObjectID = true;
                            } else {
                                throw new MalformedPIDException("Error in PID after first '%': expected '3a' or '3A', but saw '3" + c + "'.");
                            }
                        } else {
                            throw new MalformedPIDException("Error in PID after first '%': expected '3a' or '3A', but saw '" + pidString.substring(i, i+2) + "'.");
                        }
                    } else {
                        throw new MalformedPIDException("Error in PID after first '%': expected '3a' or '3A', but saw '" + pidString.substring(i+1) + "'.");
                    }
                } else if (isAlphaNum(c) || c == '-' || c == '.') {
                    out.append(c);
                } else {
                    throw new MalformedPIDException("PID namespace-id cannot contain '" + c + "' character.");
                }
            } else if (isAlphaNum(c) || c == '-' || c == '.' || c == '~' || c == '_' ) {
                out.append(c);
            } else if (c == '%') {
                if (pidString.length() >= i + 3) {
                    char h1 = getNormalizedHexChar(pidString.charAt(++i));
                    char h2 = getNormalizedHexChar(pidString.charAt(++i));
                    out.append("%" + h1 + h2);
                } else {
                    throw new MalformedPIDException("PID object-id ended early: need at least 2 chars after '%'.");
                }
            } else {
                throw new MalformedPIDException("PID object-id cannot contain '" + c + "' character.");
            }
        }
        if (!inObjectID) throw new MalformedPIDException("PID delimiter (:) is missing.");
        String outString = out.toString();
        if (outString.startsWith(":")) throw new MalformedPIDException("PID namespace-id cannot be empty.");
        if (outString.length() < 3) throw new MalformedPIDException("PID object-id cannot be empty.");
        return outString;
    }
