Path: blob/master/src/java.naming/share/classes/javax/naming/ldap/Rdn.java
41159 views
/*1* Copyright (c) 2003, 2020, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425package javax.naming.ldap;2627import java.util.Iterator;28import java.util.NoSuchElementException;29import java.util.ArrayList;30import java.util.Locale;31import java.util.Collections;3233import javax.naming.InvalidNameException;34import javax.naming.directory.BasicAttributes;35import javax.naming.directory.Attributes;36import javax.naming.directory.Attribute;37import javax.naming.NamingEnumeration;38import javax.naming.NamingException;3940import java.io.Serializable;41import java.io.ObjectOutputStream;42import java.io.ObjectInputStream;43import java.io.IOException;4445/**46* This class represents a relative distinguished name, or RDN, which is a47* component of a distinguished name as specified by48* <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>.49* An example of an RDN is "OU=Sales+CN=J.Smith". In this example,50* the RDN consist of multiple attribute type/value pairs. The51* RDN is parsed as described in the class description for52* {@link javax.naming.ldap.LdapName LdapName}.53* <p>54* The Rdn class represents an RDN as attribute type/value mappings,55* which can be viewed using56* {@link javax.naming.directory.Attributes Attributes}.57* In addition, it contains convenience methods that allow easy retrieval58* of type and value when the Rdn consist of a single type/value pair,59* which is how it appears in a typical usage.60* It also contains helper methods that allow escaping of the unformatted61* attribute value and unescaping of the value formatted according to the62* escaping syntax defined in RFC2253. For methods that take or return63* attribute value as an Object, the value is either a String64* (in unescaped form) or a byte array.65* <p>66* <code>Rdn</code> will properly parse all valid RDNs, but67* does not attempt to detect all possible violations when parsing68* invalid RDNs. It is "generous" in accepting invalid RDNs.69* The "validity" of a name is determined ultimately when it70* is supplied to an LDAP server, which may accept or71* reject the name based on factors such as its schema information72* and interoperability considerations.73*74* <p>75* The following code example shows how to construct an Rdn using the76* constructor that takes type and value as arguments:77* <pre>78* Rdn rdn = new Rdn("cn", "Juicy, Fruit");79* System.out.println(rdn.toString());80* </pre>81* The last line will print {@code cn=Juicy\, Fruit}. The82* {@link #unescapeValue(String) unescapeValue()} method can be83* used to unescape the escaped comma resulting in the original84* value {@code "Juicy, Fruit"}. The {@link #escapeValue(Object)85* escapeValue()} method adds the escape back preceding the comma.86* <p>87* This class can be instantiated by a string representation88* of the RDN defined in RFC 2253 as shown in the following code example:89* <pre>90* Rdn rdn = new Rdn("cn=Juicy\\, Fruit");91* System.out.println(rdn.toString());92* </pre>93* The last line will print {@code cn=Juicy\, Fruit}.94* <p>95* Concurrent multithreaded read-only access of an instance of96* {@code Rdn} need not be synchronized.97* <p>98* Unless otherwise noted, the behavior of passing a null argument99* to a constructor or method in this class will cause NullPointerException100* to be thrown.101*102* @since 1.5103*/104105public class Rdn implements Serializable, Comparable<Object> {106107private transient ArrayList<RdnEntry> entries;108109// The common case.110private static final int DEFAULT_SIZE = 1;111112@java.io.Serial113private static final long serialVersionUID = -5994465067210009656L;114115/**116* Constructs an Rdn from the given attribute set. See117* {@link javax.naming.directory.Attributes Attributes}.118* <p>119* The string attribute values are not interpreted as120* <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>121* formatted RDN strings. That is, the values are used122* literally (not parsed) and assumed to be unescaped.123*124* @param attrSet The non-null and non-empty attributes containing125* type/value mappings.126* @throws InvalidNameException If contents of {@code attrSet} cannot127* be used to construct a valid RDN.128*/129public Rdn(Attributes attrSet) throws InvalidNameException {130if (attrSet.size() == 0) {131throw new InvalidNameException("Attributes cannot be empty");132}133entries = new ArrayList<>(attrSet.size());134NamingEnumeration<? extends Attribute> attrs = attrSet.getAll();135try {136for (int nEntries = 0; attrs.hasMore(); nEntries++) {137RdnEntry entry = new RdnEntry();138Attribute attr = attrs.next();139entry.type = attr.getID();140entry.value = attr.get();141entries.add(nEntries, entry);142}143} catch (NamingException e) {144InvalidNameException e2 = new InvalidNameException(145e.getMessage());146e2.initCause(e);147throw e2;148}149sort(); // arrange entries for comparison150}151152/**153* Constructs an Rdn from the given string.154* This constructor takes a string formatted according to the rules155* defined in <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>156* and described in the class description for157* {@link javax.naming.ldap.LdapName}.158*159* @param rdnString The non-null and non-empty RFC2253 formatted string.160* @throws InvalidNameException If a syntax error occurs during161* parsing of the rdnString.162*/163public Rdn(String rdnString) throws InvalidNameException {164entries = new ArrayList<>(DEFAULT_SIZE);165(new Rfc2253Parser(rdnString)).parseRdn(this);166}167168/**169* Constructs an Rdn from the given {@code rdn}.170* The contents of the {@code rdn} are simply copied into the newly171* created Rdn.172* @param rdn The non-null Rdn to be copied.173*/174public Rdn(Rdn rdn) {175entries = new ArrayList<>(rdn.entries.size());176entries.addAll(rdn.entries);177}178179/**180* Constructs an Rdn from the given attribute type and181* value.182* The string attribute values are not interpreted as183* <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>184* formatted RDN strings. That is, the values are used185* literally (not parsed) and assumed to be unescaped.186*187* @param type The non-null and non-empty string attribute type.188* @param value The non-null and non-empty attribute value.189* @throws InvalidNameException If type/value cannot be used to190* construct a valid RDN.191* @see #toString()192*/193public Rdn(String type, Object value) throws InvalidNameException {194if (value == null) {195throw new NullPointerException("Cannot set value to null");196}197if (type.equals("") || isEmptyValue(value)) {198throw new InvalidNameException(199"type or value cannot be empty, type:" + type +200" value:" + value);201}202entries = new ArrayList<>(DEFAULT_SIZE);203put(type, value);204}205206private boolean isEmptyValue(Object val) {207return ((val instanceof String) && val.equals("")) ||208((val instanceof byte[]) && (((byte[]) val).length == 0));209}210211// An empty constructor used by the parser212Rdn() {213entries = new ArrayList<>(DEFAULT_SIZE);214}215216/*217* Adds the given attribute type and value to this Rdn.218* The string attribute values are not interpreted as219* <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>220* formatted RDN strings. That is the values are used221* literally (not parsed) and assumed to be unescaped.222*223* @param type The non-null and non-empty string attribute type.224* @param value The non-null and non-empty attribute value.225* @return The updated Rdn, not a new one. Cannot be null.226* @see #toString()227*/228Rdn put(String type, Object value) {229230// create new Entry231RdnEntry newEntry = new RdnEntry();232newEntry.type = type;233if (value instanceof byte[]) { // clone the byte array234newEntry.value = ((byte[]) value).clone();235} else {236newEntry.value = value;237}238entries.add(newEntry);239return this;240}241242void sort() {243if (entries.size() > 1) {244Collections.sort(entries);245}246}247248/**249* Retrieves one of this Rdn's value.250* This is a convenience method for obtaining the value,251* when the RDN contains a single type and value mapping,252* which is the common RDN usage.253* <p>254* For a multi-valued RDN, this method returns value corresponding255* to the type returned by {@link #getType() getType()} method.256*257* @return The non-null attribute value.258*/259public Object getValue() {260return entries.get(0).getValue();261}262263/**264* Retrieves one of this Rdn's type.265* This is a convenience method for obtaining the type,266* when the RDN contains a single type and value mapping,267* which is the common RDN usage.268* <p>269* For a multi-valued RDN, the type/value pairs have270* no specific order defined on them. In that case, this method271* returns type of one of the type/value pairs.272* The {@link #getValue() getValue()} method returns the273* value corresponding to the type returned by this method.274*275* @return The non-null attribute type.276*/277public String getType() {278return entries.get(0).getType();279}280281/**282* Returns this Rdn as a string represented in a format defined by283* <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a> and described284* in the class description for {@link javax.naming.ldap.LdapName LdapName}.285*286* @return The string representation of the Rdn.287*/288public String toString() {289StringBuilder builder = new StringBuilder();290int size = entries.size();291if (size > 0) {292builder.append(entries.get(0));293}294for (int next = 1; next < size; next++) {295builder.append('+');296builder.append(entries.get(next));297}298return builder.toString();299}300301/**302* Compares this Rdn with the specified Object for order.303* Returns a negative integer, zero, or a positive integer as this304* Rdn is less than, equal to, or greater than the given Object.305* <p>306* If obj is null or not an instance of Rdn, ClassCastException307* is thrown.308* <p>309* The attribute type and value pairs of the RDNs are lined up310* against each other and compared lexicographically. The order of311* components in multi-valued Rdns (such as "ou=Sales+cn=Bob") is not312* significant.313*314* @param obj The non-null object to compare against.315* @return A negative integer, zero, or a positive integer as this Rdn316* is less than, equal to, or greater than the given Object.317* @throws ClassCastException if obj is null or not a Rdn.318*/319public int compareTo(Object obj) {320if (!(obj instanceof Rdn)) {321throw new ClassCastException("The obj is not a Rdn");322}323if (obj == this) {324return 0;325}326Rdn that = (Rdn) obj;327int minSize = Math.min(entries.size(), that.entries.size());328for (int i = 0; i < minSize; i++) {329330// Compare a single pair of type/value pairs.331int diff = entries.get(i).compareTo(that.entries.get(i));332if (diff != 0) {333return diff;334}335}336return (entries.size() - that.entries.size()); // longer RDN wins337}338339/**340* Compares the specified Object with this Rdn for equality.341* Returns true if the given object is also a Rdn and the two Rdns342* represent the same attribute type and value mappings. The order of343* components in multi-valued Rdns (such as "ou=Sales+cn=Bob") is not344* significant.345* <p>346* Type and value equality matching is done as below:347* <ul>348* <li> The types are compared for equality with their case ignored.349* <li> String values with different but equivalent usage of quoting,350* escaping, or UTF8-hex-encoding are considered equal.351* The case of the values is ignored during the comparison.352* </ul>353* <p>354* If obj is null or not an instance of Rdn, false is returned.355*356* @param obj object to be compared for equality with this Rdn.357* @return true if the specified object is equal to this Rdn.358* @see #hashCode()359*/360public boolean equals(Object obj) {361if (obj == this) {362return true;363}364if (!(obj instanceof Rdn)) {365return false;366}367Rdn that = (Rdn) obj;368if (entries.size() != that.size()) {369return false;370}371for (int i = 0; i < entries.size(); i++) {372if (!entries.get(i).equals(that.entries.get(i))) {373return false;374}375}376return true;377}378379/**380* Returns the hash code of this RDN. Two RDNs that are381* equal (according to the equals method) will have the same382* hash code.383*384* @return An int representing the hash code of this Rdn.385* @see #equals386*/387public int hashCode() {388389// Sum up the hash codes of the components.390int hash = 0;391392// For each type/value pair...393for (int i = 0; i < entries.size(); i++) {394hash += entries.get(i).hashCode();395}396return hash;397}398399/**400* Retrieves the {@link javax.naming.directory.Attributes Attributes}401* view of the type/value mappings contained in this Rdn.402*403* @return The non-null attributes containing the type/value404* mappings of this Rdn.405*/406public Attributes toAttributes() {407Attributes attrs = new BasicAttributes(true);408for (int i = 0; i < entries.size(); i++) {409RdnEntry entry = entries.get(i);410Attribute attr = attrs.put(entry.getType(), entry.getValue());411if (attr != null) {412attr.add(entry.getValue());413attrs.put(attr);414}415}416return attrs;417}418419420private static class RdnEntry implements Comparable<RdnEntry> {421private String type;422private Object value;423424// If non-null, a canonical representation of the value suitable425// for comparison using String.compareTo()426private String comparable = null;427428String getType() {429return type;430}431432Object getValue() {433return value;434}435436public int compareTo(RdnEntry that) {437int diff = type.compareToIgnoreCase(that.type);438if (diff != 0) {439return diff;440}441if (value.equals(that.value)) { // try shortcut442return 0;443}444return getValueComparable().compareTo(445that.getValueComparable());446}447448public boolean equals(Object obj) {449if (obj == this) {450return true;451}452if (!(obj instanceof RdnEntry)) {453return false;454}455456// Any change here must be reflected in hashCode()457RdnEntry that = (RdnEntry) obj;458return (type.equalsIgnoreCase(that.type)) &&459(getValueComparable().equals(460that.getValueComparable()));461}462463public int hashCode() {464return (type.toUpperCase(Locale.ENGLISH).hashCode() +465getValueComparable().hashCode());466}467468public String toString() {469return type + "=" + escapeValue(value);470}471472private String getValueComparable() {473if (comparable != null) {474return comparable; // return cached result475}476477// cache result478if (value instanceof byte[]) {479comparable = escapeBinaryValue((byte[]) value);480} else {481comparable = ((String) value).toUpperCase(Locale.ENGLISH);482}483return comparable;484}485}486487/**488* Retrieves the number of attribute type/value pairs in this Rdn.489* @return The non-negative number of type/value pairs in this Rdn.490*/491public int size() {492return entries.size();493}494495/**496* Given the value of an attribute, returns a string escaped according497* to the rules specified in498* <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>.499* <p>500* For example, if the val is "Sue, Grabbit and Runn", the escaped501* value returned by this method is "Sue\, Grabbit and Runn".502* <p>503* A string value is represented as a String and binary value504* as a byte array.505*506* @param val The non-null object to be escaped.507* @return Escaped string value.508* @throws ClassCastException if val is not a String or byte array.509*/510public static String escapeValue(Object val) {511return (val instanceof byte[])512? escapeBinaryValue((byte[])val)513: escapeStringValue((String)val);514}515516/*517* Given the value of a string-valued attribute, returns a518* string suitable for inclusion in a DN. This is accomplished by519* using backslash (\) to escape the following characters:520* leading and trailing whitespace521* , = + < > # ; " \522*/523private static final String escapees = ",=+<>#;\"\\";524525private static String escapeStringValue(String val) {526527char[] chars = val.toCharArray();528StringBuilder builder = new StringBuilder(2 * val.length());529530// Find leading and trailing whitespace.531int lead; // index of first char that is not leading whitespace532for (lead = 0; lead < chars.length; lead++) {533if (!isWhitespace(chars[lead])) {534break;535}536}537int trail; // index of last char that is not trailing whitespace538for (trail = chars.length - 1; trail >= 0; trail--) {539if (!isWhitespace(chars[trail])) {540break;541}542}543544for (int i = 0; i < chars.length; i++) {545char c = chars[i];546if ((i < lead) || (i > trail) || (escapees.indexOf(c) >= 0)) {547builder.append('\\');548}549builder.append(c);550}551return builder.toString();552}553554/*555* Given the value of a binary attribute, returns a string556* suitable for inclusion in a DN (such as "#CEB1DF80").557* TBD: This method should actually generate the ber encoding558* of the binary value559*/560private static String escapeBinaryValue(byte[] val) {561562StringBuilder builder = new StringBuilder(1 + 2 * val.length);563builder.append("#");564565for (int i = 0; i < val.length; i++) {566byte b = val[i];567builder.append(Character.forDigit(0xF & (b >>> 4), 16));568builder.append(Character.forDigit(0xF & b, 16));569}570return builder.toString();571}572573/**574* Given an attribute value string formatted according to the rules575* specified in576* <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>,577* returns the unformatted value. Escapes and quotes are578* stripped away, and hex-encoded UTF-8 is converted to equivalent579* UTF-16 characters. Returns a string value as a String, and a580* binary value as a byte array.581* <p>582* Legal and illegal values are defined in RFC 2253.583* This method is generous in accepting the values and does not584* catch all illegal values.585* Therefore, passing in an illegal value might not necessarily586* trigger an {@code IllegalArgumentException}.587*588* @param val The non-null string to be unescaped.589* @return Unescaped value.590* @throws IllegalArgumentException When an Illegal value591* is provided.592*/593public static Object unescapeValue(String val) {594595char[] chars = val.toCharArray();596int beg = 0;597int end = chars.length;598599// Trim off leading and trailing whitespace.600while ((beg < end) && isWhitespace(chars[beg])) {601++beg;602}603604while ((beg < end) && isWhitespace(chars[end - 1])) {605--end;606}607608// Add back the trailing whitespace with a preceding '\'609// (escaped or unescaped) that was taken off in the above610// loop. Whether or not to retain this whitespace is decided below.611if (end != chars.length &&612(beg < end) &&613chars[end - 1] == '\\') {614end++;615}616if (beg >= end) {617return "";618}619620if (chars[beg] == '#') {621// Value is binary (eg: "#CEB1DF80").622return decodeHexPairs(chars, ++beg, end);623}624625// Trim off quotes.626if ((chars[beg] == '\"') && (chars[end - 1] == '\"')) {627++beg;628--end;629}630631StringBuilder builder = new StringBuilder(end - beg);632int esc = -1; // index of the last escaped character633634for (int i = beg; i < end; i++) {635if ((chars[i] == '\\') && (i + 1 < end)) {636if (!Character.isLetterOrDigit(chars[i + 1])) {637++i; // skip backslash638builder.append(chars[i]); // snarf escaped char639esc = i;640} else {641642// Convert hex-encoded UTF-8 to 16-bit chars.643byte[] utf8 = getUtf8Octets(chars, i, end);644if (utf8.length > 0) {645try {646builder.append(new String(utf8, "UTF8"));647} catch (java.io.UnsupportedEncodingException e) {648// shouldn't happen649}650i += utf8.length * 3 - 1;651} else { // no utf8 bytes available, invalid DN652653// '/' has no meaning, throw exception654throw new IllegalArgumentException(655"Not a valid attribute string value:" +656val + ",improper usage of backslash");657}658}659} else {660builder.append(chars[i]); // snarf unescaped char661}662}663664// Get rid of the unescaped trailing whitespace with the665// preceding '\' character that was previously added back.666int len = builder.length();667if (isWhitespace(builder.charAt(len - 1)) && esc != (end - 1)) {668builder.setLength(len - 1);669}670return builder.toString();671}672673674/*675* Given an array of chars (with starting and ending indexes into it)676* representing bytes encoded as hex-pairs (such as "CEB1DF80"),677* returns a byte array containing the decoded bytes.678*/679private static byte[] decodeHexPairs(char[] chars, int beg, int end) {680byte[] bytes = new byte[(end - beg) / 2];681for (int i = 0; beg + 1 < end; i++) {682int hi = Character.digit(chars[beg], 16);683int lo = Character.digit(chars[beg + 1], 16);684if (hi < 0 || lo < 0) {685break;686}687bytes[i] = (byte)((hi<<4) + lo);688beg += 2;689}690if (beg != end) {691throw new IllegalArgumentException(692"Illegal attribute value: " + new String(chars));693}694return bytes;695}696697/*698* Given an array of chars (with starting and ending indexes into it),699* finds the largest prefix consisting of hex-encoded UTF-8 octets,700* and returns a byte array containing the corresponding UTF-8 octets.701*702* Hex-encoded UTF-8 octets look like this:703* \03\B1\DF\80704*/705private static byte[] getUtf8Octets(char[] chars, int beg, int end) {706byte[] utf8 = new byte[(end - beg) / 3]; // allow enough room707int len = 0; // index of first unused byte in utf8708709while ((beg + 2 < end) &&710(chars[beg++] == '\\')) {711int hi = Character.digit(chars[beg++], 16);712int lo = Character.digit(chars[beg++], 16);713if (hi < 0 || lo < 0) {714break;715}716utf8[len++] = (byte)((hi<<4) + lo);717}718if (len == utf8.length) {719return utf8;720} else {721byte[] res = new byte[len];722System.arraycopy(utf8, 0, res, 0, len);723return res;724}725}726727/*728* Best guess as to what RFC 2253 means by "whitespace".729*/730private static boolean isWhitespace(char c) {731return (c == ' ' || c == '\r');732}733734/**735* The writeObject method is called to save the state of the736* {@code Rdn} to a stream.737*738* Serializes only the unparsed RDN, for compactness and to avoid739* any implementation dependency.740*741* @serialData The unparsed RDN {@code String} representation.742*743* @param s the {@code ObjectOutputStream} to write to744* @throws java.io.IOException if an I/O error occurs745*/746@java.io.Serial747private void writeObject(ObjectOutputStream s)748throws java.io.IOException {749s.defaultWriteObject();750s.writeObject(toString());751}752753/**754* The readObject method is called to restore the state of755* the {@code Rdn} from a stream.756*757* See {@code writeObject} for a description of the serial form.758*759* @param s the {@code ObjectInputStream} to read from760* @throws IOException if an I/O error occurs761* @throws ClassNotFoundException if the class of a serialized object762* could not be found763*/764@java.io.Serial765private void readObject(ObjectInputStream s)766throws IOException, ClassNotFoundException {767s.defaultReadObject();768entries = new ArrayList<>(DEFAULT_SIZE);769String unparsed = (String) s.readObject();770try {771(new Rfc2253Parser(unparsed)).parseRdn(this);772} catch (InvalidNameException e) {773// shouldn't happen774throw new java.io.StreamCorruptedException(775"Invalid name: " + unparsed);776}777}778}779780781