/******************************************************************************* * Copyright (c) 2008, 2014 Wind River Systems, Inc. and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Wind River Systems - initial API and implementation *******************************************************************************/ package org.eclipse.tcf.util; import org.eclipse.tcf.protocol.IChannel; import org.eclipse.tcf.protocol.IToken; import org.eclipse.tcf.protocol.Protocol; /** * Objects of this class are used to cache TCF remote data. * The cache is asynchronous state machine. The states are: * 1. Valid - cache is in sync with remote data, use getError() and getData() to get cached data; * 2. Invalid - cache is out of sync, start data retrieval by calling validate(); * 3. Pending - cache is waiting result of a command that was sent to remote peer; * 4. Disposed - cache is disposed and cannot be used to store data. * * A cache instance can be created on any data type that needs to be cached. * Examples might be context children list, context properties, context state, memory data, * register data, symbol, variable, etc. * Clients of cache items can register for cache changes, but don't need to think about any particular events * since that is handled by the cache item itself. * * A typical cache client should implement Runnable interface. * The implementation of run() method should: * * Validate all cache items required for client task. * If anything is invalid then client should not alter any shared data structures, * should discard any intermediate results and register (wait) for changes of invalid cache instance(s) state. * When cache item state changes, client is invoked again and full validation is restarted. * Once everything is valid, client completes its task in a single dispatch cycle. * * Note: clients should never retain copies of remote data across dispatch cycles! * Such data would get out of sync and compromise data consistency. * All remote data and everything derived from remote data should be kept in cache items * that implement proper event handling and can keep data consistent across dispatch cycles. * * @param <V> - type of data to be stored in the cache. */ public abstract class TCFDataCache<V> implements Runnable { private final int WAITING_LIST_SIZE = 8; private Throwable error; private boolean valid; private boolean posted; private boolean disposed; private V data; protected final IChannel channel; protected IToken command; private Runnable[] waiting_list = null; private int waiting_cnt; public TCFDataCache(IChannel channel) { assert channel != null; this.channel = channel; } /** * @since 1.2 */ public void post() { if (posted) return; if (waiting_cnt == 0) return; Protocol.invokeLater(this); posted = true; } /** * @return true if cache contains up-to-date data or error. */ public boolean isValid() { return valid; } /** * @return true if data retrieval command is in progress. */ public boolean isPending() { return command != null; } /** * @return true if cache is disposed. */ public boolean isDisposed() { return disposed; } /** * @return error object if data retrieval ended with an error, or null if retrieval was successful. * Note: It is prohibited to call this method when cache is not valid. */ public Throwable getError() { assert valid; return error; } /** * @return cached data object. * Note: It is prohibited to call this method when cache is not valid. */ public V getData() { assert Protocol.isDispatchThread(); assert valid; return data; } /** * Notify waiting clients about cache state change and remove them from wait list. * It is responsibility of clients to check if the state change was one they are waiting for. * Clients are not intended to call this method, they can call post() if necessary. */ public final void run() { assert Protocol.isDispatchThread(); posted = false; if (waiting_cnt > 0) { int cnt = waiting_cnt; Runnable[] arr = waiting_list; waiting_list = null; waiting_cnt = 0; for (int i = 0; i < cnt; i++) { Runnable r = arr[i]; if (r instanceof TCFDataCache<?>) { TCFDataCache<?> c = (TCFDataCache<?>)r; if (!c.posted && c.waiting_cnt > 0) { Protocol.invokeLater(c); c.posted = true; } } else if (r instanceof TCFTask<?>) { TCFTask<?> t = (TCFTask<?>)r; if (!t.isDone()) t.run(); } else { r.run(); } arr[i] = null; } if (waiting_list == null && arr.length <= WAITING_LIST_SIZE) waiting_list = arr; } } /** * Add a client call-back to cache wait list. * Client call-backs are activated when cache state changes. * Call-backs are removed from waiting list after that. * It is responsibility of clients to check if the state change was one they are waiting for. * @param cb - a call-back object */ public void wait(Runnable cb) { assert Protocol.isDispatchThread(); assert !disposed; assert !valid; if (cb != null && !isWaiting(cb)) { if (waiting_list == null) waiting_list = new Runnable[WAITING_LIST_SIZE]; if (waiting_cnt >= waiting_list.length) { Runnable[] tmp = new Runnable[waiting_cnt * 2]; System.arraycopy(waiting_list, 0, tmp, 0, waiting_list.length); waiting_list = tmp; } waiting_list[waiting_cnt++] = cb; } } /** * Return true if a client call-back is waiting for state changes of this cache item. * @param cb - a call-back object. * @return true if 'cb' is in the wait list. */ public boolean isWaiting(Runnable cb) { for (int i = 0; i < waiting_cnt; i++) { if (waiting_list[i] == cb) return true; } return false; } /** * Initiate data retrieval if the cache is not valid. * @return true if the cache is already valid */ public boolean validate() { assert Protocol.isDispatchThread(); if (disposed || channel.getState() != IChannel.STATE_OPEN) { command = null; valid = true; error = null; data = null; } else { if (command != null) return false; if (!valid && !startDataRetrieval()) return false; } assert valid; assert command == null; post(); return true; } /** * If the cache is not valid, initiate data retrieval and * add a client call-back to cache wait list. * Client call-backs are activated when cache state changes. * Call-backs are removed from waiting list after that. * It is responsibility of clients to check if the state change was one they are waiting for. * If the cache is valid do nothing and return true. * @param cb - a call-back object * @return true if the cache is already valid */ public boolean validate(Runnable cb) { if (!validate()) { wait(cb); return false; } return true; } /** * Start cache pending state. * Pending state is when the cache is waiting for a TCF command to return results. * @param command - TCF command handle. */ public void start(IToken command) { assert !valid; assert command != null; assert this.command == null; this.command = command; } /** * End cache pending state, but not mark the cache as valid. * @param command - TCF command handle. */ public void done(IToken command) { if (this.command != command) return; assert !valid; this.command = null; post(); } /** * End cache pending state and mark the cache as valid. * If 'token' != null, the data represent results from a completed command. * The data should be ignored if current cache pending command is not same as 'token'. * It can happen if the cache was reset or canceled during data retrieval. * @param token - pending command handle or null. * @param error - data retrieval error or null * @param data - up-to-date data object */ public void set(IToken token, Throwable error, V data) { assert Protocol.isDispatchThread(); if (token != null && command != token) return; command = null; if (!disposed) { assert !valid; if (channel.getState() != IChannel.STATE_OPEN) { error = null; data = null; } this.error = error; this.data = data; valid = true; } post(); } /** * Force cache to become valid, cancel pending data retrieval if any. * @param data - up-to-date data object */ public void reset(V data) { assert Protocol.isDispatchThread(); if (command != null) { command.cancel(); command = null; } if (!disposed) { this.data = data; error = null; valid = true; } post(); } /** * Invalidate the cache. * If retrieval is in progress - let it continue. */ public void reset() { assert Protocol.isDispatchThread(); if (!disposed) { error = null; valid = false; data = null; } post(); } /** * Invalidate the cache. * Cancel pending data retrieval if any. */ public void cancel() { reset(); if (command != null) { command.cancel(); command = null; } } /** * Dispose the cache. * Cancel pending data retrieval if any. */ public void dispose() { cancel(); valid = true; disposed = true; } @Override public String toString() { StringBuffer bf = new StringBuffer(); bf.append('['); if (valid) bf.append("valid,"); if (disposed) bf.append("disposed,"); if (posted) bf.append("posted,"); if (error != null) bf.append("error,"); bf.append("data="); bf.append(data == null ? "null" : data.toString()); bf.append(']'); return bf.toString(); } /** * Sub-classes should override this method to implement actual data retrieval logic. * @return true is all done, false if retrieval is in progress. */ protected abstract boolean startDataRetrieval(); }