/* * ==================== * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2008-2009 Sun Microsystems, Inc. All rights reserved. * * The contents of this file are subject to the terms of the Common Development * and Distribution License("CDDL") (the "License"). You may not use this file * except in compliance with the License. * * You can obtain a copy of the License at * http://opensource.org/licenses/cddl1.php * See the License for the specific language governing permissions and limitations * under the License. * * When distributing the Covered Code, include this CDDL Header Notice in each file * and include the License file at http://opensource.org/licenses/cddl1.php. * If applicable, add the following below this CDDL Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * ==================== * Portions Copyrighted 2010-2013 ForgeRock AS. */ package org.identityconnectors.framework.impl.api; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.identityconnectors.common.Assertions; import org.identityconnectors.common.logging.Log; import org.identityconnectors.framework.api.operations.APIOperation; import org.identityconnectors.framework.common.exceptions.ConnectorException; import org.identityconnectors.framework.common.exceptions.OperationTimeoutException; public class BufferedResultsProxy implements InvocationHandler { private final static Log LOG = Log.getLog(BufferedResultsProxy.class); private final Object target; private final int bufferSize; private final long timeoutMillis; public BufferedResultsProxy(Object target, int bufferSize, long timeoutMillis) { if (target == null) { throw new IllegalArgumentException("Target argument must not be null!"); } this.target = target; if (timeoutMillis == APIOperation.NO_TIMEOUT) { this.timeoutMillis = Long.MAX_VALUE; } else if (timeoutMillis == 0) { this.timeoutMillis = 60 * 1000; } else { this.timeoutMillis = timeoutMillis; } // create the pipe between the consumer thread an caller.. this.bufferSize = (bufferSize < 1) ? 100 : bufferSize; } private static class BufferedResultsHandler extends Thread implements ObjectStreamHandler { private static final Object DONE = new Object(); private final AtomicBoolean stopped = new AtomicBoolean(false); private final Method method; private final Object target; private final Object[] arguments; private final long timeoutMillis; private final ArrayBlockingQueue<Object> buffer; private Object result = null; public BufferedResultsHandler(Method method, Object target, Object[] arguments, int bufferSize, long timeoutMillis) { this.method = method; this.target = target; this.arguments = arguments; buffer = new ArrayBlockingQueue<Object>(bufferSize); this.timeoutMillis = timeoutMillis; } public boolean handle(final Object obj) { if (isStopped()) { return false; } Assertions.nullCheck(obj, "obj"); try { buffer.put(obj); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw ConnectorException.wrap(e); } return !isStopped(); } /** * Stops the thread and optionally waits for it to finish. * * @param wait * True if we should wait for the thread to finish * @throws OperationTimeoutException * If we said to wait and we timed out. */ public void stop(boolean wait) { if (wait && Thread.currentThread() == this) { throw new IllegalStateException("A thread cannot wait on itself"); } if (stopped.compareAndSet(false, true)) { // clear out the queue - this will cause the thread to // wakeup so that it can exit buffer.clear(); if (wait) { try { // join with a time-limit. this may timeout // if we are blocked in the producer join(timeoutMillis); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw ConnectorException.wrap(e); } // if we're still alive, we've timed out if (isAlive()) { throw new OperationTimeoutException(); } } } } public boolean isStopped() { return stopped.get(); } private Object[] createActualArguments() { Object[] actualArguments = new Object[arguments.length]; Class<?>[] paramTypes = method.getParameterTypes(); for (int i = 0; i < paramTypes.length; i++) { Class<?> paramType = paramTypes[i]; if (StreamHandlerUtil.isAdaptableToObjectStreamHandler(paramType)) { actualArguments[i] = StreamHandlerUtil.adaptFromObjectStreamHandler(paramType, this); } else { actualArguments[i] = arguments[i]; } } return actualArguments; } @Override public void run() { try { try { result = method.invoke(target, createActualArguments()); buffer.put(DONE); } catch (RuntimeException e) { buffer.put(e); } catch (InvocationTargetException e) { buffer.put(e.getTargetException()); } catch (InterruptedException e) { throw e; } catch (Exception e) { buffer.put(ConnectorException.wrap(e)); } } catch (InterruptedException e) { LOG.error(e, null); } } /** * Returns the next object from the stream. Returns null if done. * * @return The next object from the stream or null if done * @throws OperationTimeoutException * If we timed out * @throws RuntimeException * If the search threw an exception */ public Object getNextObject() { if (isStopped()) { return null; } Object obj; try { obj = buffer.poll(timeoutMillis, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw ConnectorException.wrap(e); } if (obj == null) { stop(false); // stop, but don't wait since we've already timed // out throw new OperationTimeoutException(); } else if (obj == DONE) { stop(true); // stop and wait return null; } else if (obj instanceof RuntimeException) { stop(true); // stop and wait throw (RuntimeException) obj; } else { return obj; } } private Object getResult() { return result; } } public Object invoke(final Object proxy, final Method method, Object[] arguments) throws Throwable { // do not buffer/timeout equals, hashCode, toString if (method.getDeclaringClass() == Object.class) { return method.invoke(target, arguments); } BufferedResultsHandler bufHandler = new BufferedResultsHandler(method, target, arguments, bufferSize, timeoutMillis); ObjectStreamHandler handler = null; Class<?>[] paramTypes = method.getParameterTypes(); for (int i = 0; i < paramTypes.length; i++) { Class<?> paramType = paramTypes[i]; if (StreamHandlerUtil.isAdaptableToObjectStreamHandler(paramType)) { if (handler != null) { throw new UnsupportedOperationException( "We only support operations that have a single stream handler " + method); } handler = StreamHandlerUtil.adaptToObjectStreamHandler(paramType, arguments[i]); } } if (handler == null) { throw new UnsupportedOperationException( "We only support operations that have a single stream handler " + method); } // this guy will automatically inherit // CurrentLocale since we are using a new thread // NOTE: if we ever introduce thread pooling // here, it needs to explicitly propagate bufHandler.setDaemon(true); bufHandler.start(); while (!bufHandler.isStopped()) { Object obj = bufHandler.getNextObject(); if (obj != null) { try { boolean keepGoing = handler.handle(obj); if (!keepGoing) { // stop and wait bufHandler.stop(true); } } catch (RuntimeException e) { // handler threw an exception try { // stop the buf handler thread bufHandler.stop(true); } catch (RuntimeException e2) { // log timeout if it happens, but don't mask // original exception LOG.error(e2, null); } // throw the exception the handler threw throw e; } } } return bufHandler.getResult(); } }