The framework has it's own Filter implementation which is really strict/limited. The OR/AND Filters are organised into a binary tree and the Filter implementation itself is force the case sensitive comparison which is incompatible with most of the resource. I suggest a more flexible implementation with Visitor pattern instead of a FilterTranslator where the framework wants to do too much. See QueryFilter
I would like to add the pagination support to the query, solve the problem with the case sensitive Filtered result handler, support Async third-party libraries.
See below the proposed API samples:
What's new:
New QueryResultHandler class has two methods, #handleResource to handle the ConnectorObject and the #handleResult to signal the Framework the operation is finished.
The #handleError method is https://connid.atlassian.net/wiki/display/BASE/Exception+Management to discuss
In 1.1 if the operation is finished then it cut's the QueryResultHandler so if the third party library supports the async query the dispatcher thread must be blocked. Now the call of #handleError or #handleResult would finish the operation and not block the dispatcher thread.
New SortKey to which can be used to specify the order in which ConnectorObject should be handled by the QueryResultHandler
Modify the OperationOptions or use the QueryOptions to support the pagination.
The #executeQuery(QueryFilter query, QueryResultHandler handler, QueryOptions options) method does not have the ObjectClass to support the polymorphic query. The QueryFilter use the Visitor patter and no need for the Query Translator any more. We need to support Case-Sensitive and Case-Insensitive search and the Framework can provide two Visitor implementation to do so and a third one to do a SchemaAware filtering if we add to the AttributeInfo the case-sensitive flag.
As we provide these two implementation the connector implementor can implement its own Visitor to check the Resource object before handle it or as now the FilterTranslator it can translate to NativeQuery. It gives more freedom to the Connector Developer to select any of these options and make simpler implement then now.
package org.identityconnectors.framework.spi.operations; import java.util.Collections; import java.util.List; import java.util.Map; import org.identityconnectors.framework.common.objects.AttributeInfo; import org.identityconnectors.framework.common.objects.ConnectorObject; import org.identityconnectors.framework.common.objects.OperationOptions; import org.identityconnectors.framework.common.objects.QueryResult; import org.identityconnectors.framework.common.objects.ResultHandler; import org.identityconnectors.framework.common.objects.filter.QueryFilter; /** * Implement this interface to allow the Connector to search for connector * objects. * */ public interface QueryOp extends SPIOperation { /** * A completion handler for consuming the results of a query request. * <p> * A query result completion handler may be specified when performing query * requests using a * {@link org.identityconnectors.framework.api.ConnectorFacade} object. The * {@link #handleResource} method is invoked each time a matching * {@link ConnectorObject} resource is returned, followed by * {@link #handleResult} or {@link #handleError} indicating that no more * ConnectorObject resources will be returned. * <p> * Implementations of these methods should complete in a timely manner so as * to avoid keeping the invoking thread from dispatching to other completion * handlers. */ public interface QueryResultHandler extends ResultHandler<QueryResult> { /** * {@inheritDoc} */ void handleError(RuntimeException error); /** * Invoked each time a matching ConnectorObject is returned from a query * request. * * @param resource * The matching ConnectorObject. * @return {@code true} if this handler should continue to be notified * of any remaining matching ConnectorObjects, or {@code false} * if the remaining ConnectorObjects should be skipped for some * reason (e.g. a client side size limit has been reached). */ boolean handleResource(ConnectorObject resource); /** * {@inheritDoc} * * @param result * The query result indicating that no more resources are to * be returned and, if applicable, including information * which should be used for subsequent paged results query * requests. */ void handleResult(QueryResult result); } /** * A sort key which can be used to specify the order in which * ConnectorObject should be included in the results of a query request. */ public final class SortKey { private final AttributeInfo field; private final boolean isAscendingOrder; public SortKey(final AttributeInfo field, final boolean isAscendingOrder) { this.field = field; this.isAscendingOrder = isAscendingOrder; } /** * Returns the sort key field. * * @return The sort key field. */ public AttributeInfo getField() { return field; } /** * Returns {@code true} if this sort key is in ascending order, or * {@code false} if it is in descending order. * * @return {@code true} if this sort key is in ascending order, or * {@code false} if it is in descending ord)er. */ public boolean isAscendingOrder() { return isAscendingOrder; } } public class QueryOptions extends OperationOptions { public QueryOptions(Map<String, Object> operationOptions) { super(operationOptions); } /** * Returns the opaque cookie which is used by the resource provider to * track its position in the set of query results. Paged results will be * enabled if and only if the page size is non-zero. * <p> * The cookie must be {@code null} in the initial query request sent by * the client. For subsequent query requests the client must include the * cookie returned with the previous query result, until the resource * provider returns a {@code null} cookie indicating that the final page * of results has been returned. * * @return The opaque cookie which is used by the resource provider to * track its position in the set of query results, or * {@code null} if paged results are not requested (when the * page size is 0), or if the first page of results is being * requested (when the page size is non-zero). * @see #getPageSize() * @see #getPagedResultsOffset() */ String getPagedResultsCookie() { return "1234"; }; /** * Returns the index within the result set of the first result which * should be returned. Paged results will be enabled if and only if the * page size is non-zero. If the parameter is not present or a value * less than 1 is specified then then the page following the previous * page returned will be returned. A value equal to or greater than 1 * indicates that a specific page should be returned starting from the * position specified. * * @return The index within the result set of the first result which * should be returned. * @see #getPageSize() * @see #getPagedResultsCookie() */ int getPagedResultsOffset() { return 0; }; /** * Returns the requested page results page size or {@code 0} if paged * results are not required. For all paged result requests other than * the initial request, a cookie should be provided with the query * request. See {@link #getPagedResultsCookie()} for more information. * * @return The requested page results page size or {@code 0} if paged * results are not required. * @see #getPagedResultsCookie() * @see #getPagedResultsOffset() */ int getPageSize() { return 100; }; /** * Returns the sort keys which should be used for ordering the JSON * resources returned by this query request. The returned list may be * modified if permitted by this query request. * * @return The sort keys which should be used for ordering the JSON * resources returned by this query request (never {@code null} * ). */ List<SortKey> getSortKeys() { return Collections.emptyList(); }; } /** * ConnectorFacade calls this method once for each native query. * * @param query * The native query to run. A value of null means * "return every instance of the given object class". * @param handler * Results should be returned to this handler * @param options * Additional options that impact the way this operation is run. * If the caller passes null, the framework will convert this * into an empty set of options, so SPI need not guard against * options being null. */ public void executeQuery(QueryFilter query, QueryResultHandler handler, QueryOptions options); }
What's new:
New SyncResultHandler class has two methods, #handleSyncDelta to handle the SyncDelta and the #handleResult to signal the Framework the operation is finished.
The #handleError method is https://connid.atlassian.net/wiki/display/BASE/Exception+Management to discuss
In 1.1 if the operation is finished then it cut's the SyncResultsHandler and it's has the same async problem as query.
Sync has additional problem because if all change was filtered out then it can not update the IDM with the latest SyncToken and it well restart the processing from the same point until at least one SyncDelta is processed.
It allows to do a multi ObjectClass syncronization and because the OperationOptions is extended with the pageSize sortKey it allows to customise more the orders or related events. (Sounds too extreme but I have real use-case where is would solve the problem)
To allow simple migration from 1.1 to 1.4 There will be a util method:
/** * Extracts the ObjectClass, SyncToken pairs from the filter parameter. * * This util method in FrameworkUtils gets the ObjectClass, SyncToken form * the query and the old 1.1 API can be used still. * * @param filter * @return */ public Set<Pair<ObjectClass, SyncToken>> extract(QueryFilter filter);
package org.identityconnectors.framework.spi.operations; import java.util.Map; import java.util.Set; import org.identityconnectors.common.Pair; import org.identityconnectors.framework.common.objects.ObjectClass; import org.identityconnectors.framework.common.objects.OperationOptions; import org.identityconnectors.framework.common.objects.ResultHandler; import org.identityconnectors.framework.common.objects.SyncDelta; import org.identityconnectors.framework.common.objects.SyncResultsHandler; import org.identityconnectors.framework.common.objects.SyncToken; import org.identityconnectors.framework.common.objects.filter.QueryFilter; /** * Poll for synchronization events--i.e., native changes to target objects. * */ public interface SynchronizeOp extends SPIOperation { /** * A completion handler for consuming the results of a synchronization * request. * <p> * A synchronization result completion handler may be specified when * performing synchronization requests using a * {@link org.identityconnectors.framework.api.ConnectorFacade} object. The * {@link #handleSyncDelta} method is invoked each time a matching * {@link SyncDelta} resource is returned, followed by {@link #handleResult} * or {@link #handleError} indicating that no more ConnectorObject resources * will be returned. * <p> * Implementations of these methods should complete in a timely manner so as * to avoid keeping the invoking thread from dispatching to other completion * handlers. */ public interface SyncResultHandler extends ResultHandler<SyncToken> { /** * {@inheritDoc} */ void handleError(RuntimeException error); /** * Invoked each time a matching ConnectorObject is returned from a query * request. * * @param resource * The matching ConnectorObject. * @return {@code true} if this handler should continue to be notified * of any remaining matching ConnectorObjects, or {@code false} * if the remaining ConnectorObjects should be skipped for some * reason (e.g. a client side size limit has been reached). */ boolean handleSyncDelta(SyncDelta resource); /** * {@inheritDoc} * * @param result * The query result indicating that no more resources are to * be returned and, if applicable, including information * which should be used for subsequent paged results query * requests. */ void handleResult(SyncToken result); } /** * Request synchronization events--i.e., native changes to target objects. * <p> * This method will call the specified * {@linkplain org.identityconnectors.framework.common.objects.SyncResultsHandler#handle * handler} once to pass back each matching * {@linkplain org.identityconnectors.framework.common.objects.SyncDelta * synchronization event}. Once this method returns, this method will no * longer invoke the specified handler. * <p> * Each * {@linkplain org.identityconnectors.framework.common.objects.SyncDelta#getToken() * synchronization event contains a token} that can be used to resume * reading events <i>starting from that point in the event stream</i>. In * typical usage, a client will save the token from the final * synchronization event that was received from one invocation of this * {@code sync()} method and then pass that token into that client's next * call to this {@code sync()} method. This allows a client to * "pick up where he left off" in receiving synchronization events. However, * a client can pass the token from <i>any</i> synchronization event into a * subsequent invocation of this {@code sync()} method. This will return * synchronization events (that represent native changes that occurred) * immediately subsequent to the event from which the client obtained the * token. * <p> * A client that wants to read synchronization events "starting now" can * call {@link #getLatestSyncToken} and then pass that token into this * {@code sync()} method. * * @param filter * The filter which used to select which ConnectorObject should * be included. It includes the ObjectClass and the Token pair * where token representing the last token from the previous * sync. The {@code SyncResultsHandler} will return any number of * {@linkplain org.identityconnectors.framework.common.objects.SyncDelta} * objects, each of which contains a token. Should be * {@code null} if this is the client's first call to the * {@code sync()} method for this connector. * @param handler * The result handler. Must not be null. * @param options * Options that affect the way this operation is run. If the * caller passes {@code null}, the framework will convert this * into an empty set of options, so an implementation need not * guard against this being null. * @throws IllegalArgumentException * if {@code objectClass} or {@code handler} is null or if any * argument is invalid. */ public void sync(QueryFilter filter, SyncResultsHandler handler, OperationOptions options); /** * Returns the token corresponding to the most recent synchronization event. * <p> * An application that wants to receive synchronization events * "starting now" --i.e., wants to receive only native changes that occur * after this method is called-- should call this method and then pass the * resulting token into {@linkplain #sync the sync() method}. * * @param objectClass * the class of object for which to find the most recent * synchronization event (if any). Must not be null. * @return A token if synchronization events exist; otherwise {@code null}. * @throws IllegalArgumentException * if {@code objectClass} is null or is invalid. */ public SyncToken getLatestSyncToken(ObjectClass objectClass); }