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 2016 ForgeRock AS.
015 */
016package org.forgerock.opendj.examples;
017
018import static org.forgerock.util.Utils.closeSilently;
019import org.forgerock.opendj.ldap.Connection;
020import org.forgerock.opendj.ldap.LDAPConnectionFactory;
021import org.forgerock.opendj.ldap.LdapException;
022import org.forgerock.opendj.ldap.ModificationType;
023import org.forgerock.opendj.ldap.ResultCode;
024import org.forgerock.opendj.ldap.RootDSE;
025import org.forgerock.opendj.ldap.controls.PermissiveModifyRequestControl;
026import org.forgerock.opendj.ldap.requests.Requests;
027import org.forgerock.opendj.ldap.responses.BindResult;
028import org.forgerock.opendj.ldap.responses.CompareResult;
029import org.forgerock.opendj.ldap.responses.Responses;
030import org.forgerock.opendj.ldap.responses.Result;
031import org.forgerock.util.AsyncFunction;
032import org.forgerock.util.promise.ExceptionHandler;
033import org.forgerock.util.promise.Promise;
034import org.forgerock.util.promise.Promises;
035import org.forgerock.util.promise.ResultHandler;
036
037import java.util.concurrent.CountDownLatch;
038
039/**
040 * This command-line client demonstrates adding and removing a member from a
041 * (potentially large) static group using the asynchronous APIs.
042 *
043 * The client takes as arguments the host and port of the directory server, the
044 * group DN, the member DN, and whether to "add" or "del" the specified member
045 * from the group. The client uses the Permissive Modify control if it is
046 * available to avoid having to check whether the member belongs to the group or
047 * not.
048 *
049 * This client expects a group that is a <code>groupOfNames</code> such as:
050 *
051 * <pre>
052 * dn: cn=My Static Group,ou=Groups,dc=example,dc=com
053 * cn: My Static Group
054 * objectClass: groupOfNames
055 * objectClass: top
056 * ou: Groups
057 * member: uid=ahunter,ou=People,dc=example,dc=com
058 * member: uid=bjensen,ou=People,dc=example,dc=com
059 * member: uid=tmorris,ou=People,dc=example,dc=com
060 * </pre>
061 *
062 * This client connects as <code>cn=Directory Manager</code> with password
063 * <code>password</code>. Not a best practice; in real code use application
064 * specific credentials to connect, and ensure that your application has access
065 * to use the Permissive Modify control if your directory server supports it.
066 */
067public final class UpdateGroupAsync {
068    /** Connection to the LDAP server. */
069    private static Connection connection;
070    /** Result for the operation. */
071    private static int resultCode;
072    /** Count down latch to wait for modify operation to complete. */
073    private static final CountDownLatch COMPLETION_LATCH = new CountDownLatch(1);
074
075    /**
076     * Updates the group as necessary.
077     *
078     * @param args
079     *            The command line arguments: host, port, group-dn, member-dn, (add|del)
080     */
081    public static void main(String[] args) {
082        if (args.length != 5) {
083            printUsage();
084        }
085        final String host              = args[0];
086        final int port                 = Integer.parseInt(args[1]);
087        final String groupDn           = args[2];
088        final String memberDn          = args[3];
089        final ModificationType modType = getModificationType(args[4]);
090
091        // Connect, bind, update group.
092        new LDAPConnectionFactory(host, port)
093                .getConnectionAsync()
094                .thenAsync(new AsyncFunction<Connection, BindResult, LdapException>() {
095                    @Override
096                    public Promise<BindResult, LdapException> apply(Connection connection)
097                            throws LdapException {
098                        UpdateGroupAsync.connection = connection;
099                        return connection.bindAsync(
100                                Requests.newSimpleBindRequest("cn=Directory Manager", "password".toCharArray()));
101                    }
102                })
103                .thenAsync(new AsyncFunction<BindResult, RootDSE, LdapException>() {
104                    @Override
105                    public Promise<RootDSE, LdapException> apply(BindResult bindResult)
106                            throws LdapException {
107                        return RootDSE.readRootDSEAsync(connection);
108                    }
109                })
110                .thenAsync(new AsyncFunction<RootDSE, Result, LdapException>() {
111                    @Override
112                    public Promise<Result, LdapException> apply(RootDSE rootDSE) throws LdapException {
113                        // If the directory supports the Permissive Modify request control,
114                        // then the modification type does not matter.
115                        if (rootDSE.getSupportedControls().contains(PermissiveModifyRequestControl.OID)) {
116                            log("Updating group membership.");
117                            return connection.modifyAsync(
118                                    Requests.newModifyRequest(groupDn)
119                                            .addControl(PermissiveModifyRequestControl.newControl(true))
120                                            .addModification(modType, "member", memberDn));
121                        } else {
122                            return connection
123                                    // Check whether the member is present.
124                                    .compareAsync(Requests.newCompareRequest(groupDn, "member", memberDn))
125                                    .thenAsync(new AsyncFunction<CompareResult, Result, LdapException>() {
126                                        @Override
127                                        public Promise<Result, LdapException> apply(CompareResult compareResult)
128                                                throws LdapException {
129                                            ResultCode rc = compareResult.getResultCode();
130                                            // Only add the member if missing from the group.
131                                            if (modType.equals(ModificationType.ADD)
132                                                    && rc.equals(ResultCode.COMPARE_FALSE)) {
133                                                log("Adding " + memberDn + " to " + groupDn + ".");
134                                                return connection.modifyAsync(
135                                                        Requests.newModifyRequest(groupDn)
136                                                                .addModification(modType, "member", memberDn));
137                                            // Only delete if present in the group.
138                                            } else if (modType.equals(ModificationType.DELETE)
139                                                    && rc.equals(ResultCode.COMPARE_TRUE)) {
140                                                log("Deleting " + memberDn + " from " + groupDn + ".");
141                                                return connection.modifyAsync(
142                                                        Requests.newModifyRequest(groupDn)
143                                                                .addModification(modType, "member", memberDn));
144                                            } else {
145                                                return Promises.newResultPromise(
146                                                        Responses.newResult(ResultCode.SUCCESS));
147                                            }
148                                        }
149                                    });
150                        }
151                    }
152                })
153                .thenOnResult(new ResultHandler<Result>() {
154                    @Override
155                    public void handleResult(Result result) {
156                        final String op = (modType == ModificationType.ADD) ? "added to" : "deleted from";
157                        log(memberDn + " has been " + op + " the group " + groupDn + ".");
158                        resultCode = result.getResultCode().intValue();
159                        COMPLETION_LATCH.countDown();
160                    }
161                })
162                .thenOnException(new ExceptionHandler<LdapException>() {
163                    @Override
164                    public void handleException(LdapException e) {
165                        System.err.println(e.getMessage());
166                        resultCode = e.getResult().getResultCode().intValue();
167                        COMPLETION_LATCH.countDown();
168                    }
169                });
170
171        try {
172            COMPLETION_LATCH.await();
173        } catch (InterruptedException e) {
174            System.err.println(e.getMessage());
175            System.exit(ResultCode.CLIENT_SIDE_USER_CANCELLED.intValue());
176            return;
177        }
178
179        closeSilently(connection);
180        System.exit(resultCode);
181    }
182
183    /** Print usage then exit. */
184    private static void printUsage() {
185        System.err.println("Usage: host port group-dn member-dn {add|del}");
186        System.err.println("For example: localhost 1389 "
187                + "cn=Static,ou=Groups,dc=example,dc=com "
188                + "uid=user.5150,ou=People,dc=example,dc=com "
189                + "del");
190        System.exit(1);
191    }
192
193    /**
194     * Return the modification type for the update operation.
195     * @param operation Operation specified as an argument (add or del).
196     */
197    private static ModificationType getModificationType(String operation) {
198        final boolean isAdd = "add".equalsIgnoreCase(operation);
199        if (!isAdd && !"del".equalsIgnoreCase(operation)) {
200            printUsage();
201        }
202        return isAdd ? ModificationType.ADD : ModificationType.DELETE;
203    }
204
205    /**
206     * Log a message to System.out.
207     *
208     * @param message   The message to write to the console.
209     */
210    private static void log(final String message) {
211        System.out.println(message);
212    }
213
214    /** Constructor not used. */
215    private UpdateGroupAsync() {
216        // Not used.
217    }
218}