/*
 * #%L
 * fujion
 * %%
 * Copyright (C) 2008 - 2017 Regenstrief Institute, Inc.
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * #L%
 */
package org.fujion.client;

import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.fujion.ancillary.IResponseCallback;
import org.fujion.component.Page;
import org.fujion.event.CallbackEvent;
import org.fujion.event.ITypedEventListener;

/**
 * A registry for managing pending callbacks. There is timestamp-based eviction logic for removing
 * orphaned entries.
 */
public class CallbackRegistry {

    private static final Log log = LogFactory.getLog(CallbackRegistry.class);

    private static final long HALF_HOUR = 1000 * 60 * 30;
    
    private final Map<Integer, IResponseCallback<?>> callbacks = Collections.synchronizedMap(new HashMap<>());

    private final Map<Integer, Long> timestamps = Collections.synchronizedSortedMap(new TreeMap<>());

    private final AtomicInteger callbackId = new AtomicInteger();

    private final long evictionInterval;

    private final ITypedEventListener<CallbackEvent> callbackListener = (event) -> {
        invokeCallback(event.getHandle(), event.getData());
    };

    /**
     * Creates a callback registry for the specified page using the default eviction interval (30
     * minutes).
     *
     * @param page The owning page.
     */
    public CallbackRegistry(Page page) {
        this(page, HALF_HOUR);
    }

    /**
     * Creates a callback registry for the specified page using the specified eviction interval.
     *
     * @param page The owning page.
     * @param evictionInterval The time in milliseconds after which an entry will be evicted.
     */
    public CallbackRegistry(Page page, long evictionInterval) {
        this.evictionInterval = evictionInterval;
        page.addEventListener(CallbackEvent.class, callbackListener);
    }

    /**
     * Registers a callback.
     *
     * @param callback Callback to register.
     * @return A unique handle.
     */
    public int registerCallback(IResponseCallback<?> callback) {
        int handle = callbackId.incrementAndGet();
        callbacks.put(handle, callback);
        timestamps.put(handle, System.currentTimeMillis());
        return handle;
    }

    /**
     * Unregisters a callback.
     *
     * @param handle The callback's handle.
     * @return The callback that was unregistered (may be null).
     */
    private IResponseCallback<?> unregisterCallback(int handle) {
        timestamps.remove(handle);
        IResponseCallback<?> callback = callbacks.remove(handle);
        evict();
        return callback;
    }
    
    /**
     * Unregisters and invokes a callback.
     *
     * @param handle The callback's handle.
     * @param response The response to pass to the callback.
     */
    private void invokeCallback(int handle, Object response) {
        @SuppressWarnings("unchecked")
        IResponseCallback<Object> callback = (IResponseCallback<Object>) unregisterCallback(handle);
        
        if (callback != null) {
            try {
                callback.onComplete(response);
            } catch (Exception e) {
                log.error("Exception on client callback", e);
            }
        }
    }

    /**
     * Clears all registered callbacks.
     */
    public void clear() {
        callbacks.clear();
        timestamps.clear();
    }

    /**
     * Evict entries older than the specified eviction time.
     */
    private void evict() {
        long threshold = System.currentTimeMillis() - evictionInterval;

        synchronized (timestamps) {
            Iterator<Entry<Integer, Long>> iter = timestamps.entrySet().iterator();

            while (iter.hasNext()) {
                Entry<Integer, Long> entry = iter.next();

                if (entry.getValue() < threshold) {
                    iter.remove();
                    callbacks.remove(entry.getKey());
                } else {
                    break;
                }
            }
        }
    }
}

@@ -1301,16 +1301,20 @@
 istry %7B%0A
+    
 %0A    pri
@@ -1384,16 +1384,20 @@
 class);%0A
+    
 %0A    pri
@@ -1447,20 +1447,16 @@
 0 * 30;%0A
-    
 %0A    pri
@@ -1551,32 +1551,36 @@
 w HashMap%3C%3E());%0A
+    
 %0A    private fin
@@ -1666,16 +1666,20 @@
 p%3C%3E());%0A
+    
 %0A    pri
@@ -1733,24 +1733,28 @@
 cInteger();%0A
+    
 %0A    private
@@ -1779,24 +1779,28 @@
 onInterval;%0A
+    
 %0A    private
@@ -1940,16 +1940,20 @@
 %0A    %7D;%0A
+    
 %0A    /**
@@ -2189,32 +2189,36 @@
 LF_HOUR);%0A    %7D%0A
+    
 %0A    /**%0A     * 
@@ -2634,32 +2634,36 @@
 istener);%0A    %7D%0A
+    
 %0A    /**%0A     * 
@@ -3014,32 +3014,36 @@
 n handle;%0A    %7D%0A
+    
 %0A    /**%0A     * 
@@ -3397,36 +3397,32 @@
 callback;%0A    %7D%0A
-    
 %0A    /**%0A     * 
@@ -3780,24 +3780,16 @@
 andle);%0A
-        
 %0A       
@@ -4002,24 +4002,28 @@
     %7D%0A    %7D%0A
+    
 %0A    /**%0A   
@@ -4146,32 +4146,36 @@
 .clear();%0A    %7D%0A
+    
 %0A    /**%0A     * 
@@ -4335,16 +4335,24 @@
 terval;%0A
+        
 %0A       
@@ -4460,24 +4460,36 @@
 iterator();%0A
+            
 %0A           
@@ -4572,16 +4572,32 @@
 next();%0A
+                
 %0A       
@@ -4725,24 +4725,175 @@
 y.getKey());
+%0A%0A                    if (log.isWarnEnabled()) %7B%0A                        log.warn(%22Evicted expired callback #%22 + entry.getKey());%0A                    %7D
 %0A           

Add eviction warning entry in log.