001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2010 Sun Microsystems, Inc.
015 * Portions copyright 2011-2016 ForgeRock AS.
016 */
017package org.forgerock.opendj.ldap.controls;
018
019import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
020import static com.forgerock.opendj.ldap.CoreMessages.ERR_SUBENTRIES_CANNOT_DECODE_VALUE;
021import static com.forgerock.opendj.ldap.CoreMessages.ERR_SUBENTRIES_CONTROL_BAD_OID;
022import static com.forgerock.opendj.ldap.CoreMessages.ERR_SUBENTRIES_NO_CONTROL_VALUE;
023
024import java.io.IOException;
025
026import org.forgerock.i18n.LocalizableMessage;
027import org.forgerock.i18n.slf4j.LocalizedLogger;
028import org.forgerock.opendj.io.ASN1;
029import org.forgerock.opendj.io.ASN1Reader;
030import org.forgerock.opendj.io.ASN1Writer;
031import org.forgerock.opendj.ldap.ByteString;
032import org.forgerock.opendj.ldap.ByteStringBuilder;
033import org.forgerock.opendj.ldap.DecodeException;
034import org.forgerock.opendj.ldap.DecodeOptions;
035import org.forgerock.util.Reject;
036
037/**
038 * The sub-entries request control as defined in RFC 3672. This control may be
039 * included in a search request to indicate that sub-entries should be included
040 * in the search results.
041 * <p>
042 * In the absence of the sub-entries request control, sub-entries are not
043 * visible to search operations unless the target/base of the operation is a
044 * sub-entry. In the presence of the sub-entry request control, sub-entries are
045 * visible if and only if the control's value is {@code TRUE}.
046 * <p>
047 * Consider "Class of Service" sub-entries such as the following:
048 *
049 * <pre>
050 * dn: cn=Gold Class of Service,dc=example,dc=com
051 * objectClass: collectiveAttributeSubentry
052 * objectClass: extensibleObject
053 * objectClass: subentry
054 * objectClass: top
055 * cn: Gold Class of Service
056 * diskQuota;collective: 100 GB
057 * mailQuota;collective: 10 GB
058 * subtreeSpecification: { base "ou=People", specificationFilter "(classOfService=
059 *  gold)" }
060 * </pre>
061 *
062 * To access the sub-entries in your search, use the control with value
063 * {@code TRUE}.
064 *
065 * <pre>
066 * Connection connection = ...;
067 *
068 * SearchRequest request = Requests.newSearchRequest("dc=example,dc=com",
069 *         SearchScope.WHOLE_SUBTREE, "cn=*Class of Service", "cn", "subtreeSpecification")
070 *         .addControl(SubentriesRequestControl.newControl(true, true));
071 *  
072 * ConnectionEntryReader reader = connection.search(request);
073 * while (reader.hasNext()) {
074 *     if (reader.isEntry()) {
075 *         SearchResultEntry entry = reader.readEntry();
076 *         // ...
077 *     }
078 * }
079 * </pre>
080 *
081 * @see <a href="http://tools.ietf.org/html/rfc3672">RFC 3672 - Subentries in
082 *      the Lightweight Directory Access Protocol </a>
083 */
084public final class SubentriesRequestControl implements Control {
085
086    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
087    /** The OID for the sub-entries request control. */
088    public static final String OID = "1.3.6.1.4.1.4203.1.10.1";
089
090    private static final SubentriesRequestControl CRITICAL_VISIBLE_INSTANCE =
091            new SubentriesRequestControl(true, true);
092    private static final SubentriesRequestControl NONCRITICAL_VISIBLE_INSTANCE =
093            new SubentriesRequestControl(false, true);
094    private static final SubentriesRequestControl CRITICAL_INVISIBLE_INSTANCE =
095            new SubentriesRequestControl(true, false);
096    private static final SubentriesRequestControl NONCRITICAL_INVISIBLE_INSTANCE =
097            new SubentriesRequestControl(false, false);
098
099    /** A decoder which can be used for decoding the sub-entries request control. */
100    public static final ControlDecoder<SubentriesRequestControl> DECODER =
101            new ControlDecoder<SubentriesRequestControl>() {
102
103                @Override
104                public SubentriesRequestControl decodeControl(final Control control,
105                        final DecodeOptions options) throws DecodeException {
106                    Reject.ifNull(control);
107
108                    if (control instanceof SubentriesRequestControl) {
109                        return (SubentriesRequestControl) control;
110                    }
111
112                    if (!control.getOID().equals(OID)) {
113                        final LocalizableMessage message =
114                                ERR_SUBENTRIES_CONTROL_BAD_OID.get(control.getOID(), OID);
115                        throw DecodeException.error(message);
116                    }
117
118                    if (!control.hasValue()) {
119                        // The response control must always have a value.
120                        final LocalizableMessage message = ERR_SUBENTRIES_NO_CONTROL_VALUE.get();
121                        throw DecodeException.error(message);
122                    }
123
124                    final ASN1Reader reader = ASN1.getReader(control.getValue());
125                    final boolean visibility;
126                    try {
127                        visibility = reader.readBoolean();
128                    } catch (final IOException e) {
129                        logger.debug(LocalizableMessage.raw("Unable to read visbility", e));
130                        final LocalizableMessage message =
131                                ERR_SUBENTRIES_CANNOT_DECODE_VALUE.get(getExceptionMessage(e));
132                        throw DecodeException.error(message);
133                    }
134
135                    return newControl(control.isCritical(), visibility);
136                }
137
138                @Override
139                public String getOID() {
140                    return OID;
141                }
142            };
143
144    /**
145     * Creates a new sub-entries request control having the provided criticality
146     * and sub-entry visibility.
147     *
148     * @param isCritical
149     *            {@code true} if it is unacceptable to perform the operation
150     *            without applying the semantics of this control, or
151     *            {@code false} if it can be ignored.
152     * @param visibility
153     *            {@code true} if sub-entries should be included in the search
154     *            results and normal entries excluded, or {@code false} if
155     *            normal entries should be included and sub-entries excluded.
156     * @return The new control.
157     */
158    public static SubentriesRequestControl newControl(final boolean isCritical,
159            final boolean visibility) {
160        if (isCritical) {
161            return visibility ? CRITICAL_VISIBLE_INSTANCE : CRITICAL_INVISIBLE_INSTANCE;
162        } else {
163            return visibility ? NONCRITICAL_VISIBLE_INSTANCE : NONCRITICAL_INVISIBLE_INSTANCE;
164        }
165    }
166
167    private final boolean isCritical;
168    private final boolean visibility;
169
170    private SubentriesRequestControl(final boolean isCritical, final boolean visibility) {
171        this.isCritical = isCritical;
172        this.visibility = visibility;
173    }
174
175    @Override
176    public String getOID() {
177        return OID;
178    }
179
180    @Override
181    public ByteString getValue() {
182        final ByteStringBuilder buffer = new ByteStringBuilder();
183        final ASN1Writer writer = ASN1.getWriter(buffer);
184        try {
185            writer.writeBoolean(visibility);
186            return buffer.toByteString();
187        } catch (final IOException ioe) {
188            // This should never happen unless there is a bug somewhere.
189            throw new RuntimeException(ioe);
190        }
191    }
192
193    /**
194     * Returns a boolean indicating whether sub-entries should be
195     * included in the search results.
196     *
197     * @return {@code true} if sub-entries should be included in the search
198     *         results and normal entries excluded, or {@code false} if normal
199     *         entries should be included and sub-entries excluded.
200     */
201    public boolean getVisibility() {
202        return visibility;
203    }
204
205    @Override
206    public boolean hasValue() {
207        return false;
208    }
209
210    @Override
211    public boolean isCritical() {
212        return isCritical;
213    }
214
215    @Override
216    public String toString() {
217        final StringBuilder builder = new StringBuilder();
218        builder.append("SubentriesRequestControl(oid=");
219        builder.append(getOID());
220        builder.append(", criticality=");
221        builder.append(isCritical());
222        builder.append(", visibility=");
223        builder.append(getVisibility());
224        builder.append(")");
225        return builder.toString();
226    }
227
228}