001: /*
002: * ContextProviderSupport.java
003: *
004: * Created on January 27, 2004, 3:02 PM
005: */
006:
007: package org.netbeans.actions.spi;
008:
009: import java.util.Arrays;
010: import java.util.Collections;
011: import java.util.HashSet;
012: import java.util.Iterator;
013: import java.util.Map;
014: import java.util.Set;
015: import org.netbeans.actions.api.ContextProvider;
016:
017: /** Provides a more convenient and efficient way to implement ContextProvider -
018: * assembles the context map out of a set of Contributors. Provides two
019: * implementations of Contributor, the preferred one composing its portion of
020: * the context from simple arrays of keys/values (FixedContributor) and another
021: * version which allows the application to supply its own map.
022: * <p>
023: * Note that the contributions of the various components are not checked for
024: * duplicates (this could be added at 0 cost with assertions if needed) - if
025: * there is duplication, the actual contents of the map are undefined. It is
026: * the responsibility of the implementer to ensure this does not happen.
027: * <p>
028: * One caveat to using this class - do not use the string "identity" as
029: * one of the keys for a Contributor's map (defined as ContextProvider.IDENTITY).
030: * ContextProviderSupport reserves the use of this key.
031: * <p>
032: * Note that the Maps returned by ContextProviderSupport.getContext() are
033: * immutable - put, clear, etc. will throw an UnsupportedOperationException.
034: *
035: * @author Tim Boudreau
036: */
037: public final class ContextProviderSupport implements ContextProvider {
038: private Contributor[] contributors = null;
039:
040: /** Creates a new instance of DefaultContextProvider */
041: public ContextProviderSupport() {
042: }
043:
044: public ContextProviderSupport(Contributor[] contributors) {
045: setContributors(contributors);
046: }
047:
048: /** Set the contributors that will add context information to the application
049: * context */
050: public void setContributors(Contributor[] contributors) {
051: //XXX maybe use a list argument so list can change dynamically?
052: this .contributors = contributors;
053: }
054:
055: /** Get a map composed of the contributions of each contributor */
056: public final Map getContext() {
057: if (contributors == null || contributors.length == 0) {
058: return Collections.EMPTY_MAP;
059: } else {
060: Map[] m = new Map[contributors.length];
061: for (int i = 0; i < contributors.length; i++) {
062: Map curr = contributors[i].getContribution();
063: if (curr instanceof ActiveContributor) {
064: //Avoid concurrentModificationExceptions by making a fast
065: //clone of the map
066: curr = ((ActiveContributor) curr).toFixedMap();
067: }
068: m[i] = contributors[i].getContribution();
069: }
070: return new ProxyMap(m);
071: }
072: }
073:
074: /** A contributor provides a portion of the application context used to
075: * determine action availability */
076: public interface Contributor {
077: public Map getContribution();
078: }
079:
080: /** Implementation of Contributor which takes fixed arrays of keys and
081: * values */
082: public static abstract class FixedContributor implements
083: Contributor {
084: public final Map getContribution() {
085: return new FixedMap(getKeys(), getValues());
086: }
087:
088: public abstract String[] getKeys();
089:
090: public abstract String[] getValues();
091: }
092:
093: /** Implementation of Contributor which allows dynamic creation of the
094: * map of keys and values. Where possible it is preferred to use
095: * FixedContributor - it is fairly unusual for an application to not know
096: * enough about what state it is in to do that, and it is more efficient.
097: * <p>
098: * Note that the map that will actually be used is a snapshot of the map
099: * returned, taken at the time of updating.
100: */
101: public static abstract class ActiveContributor implements
102: Contributor {
103: public abstract Map getContribution();
104:
105: Map toFixedMap() {
106: //Hmm, should we bother? This is just cloning the map. Maybe
107: //kill this class, although if someone uses a weird map impl,
108: //this avoid concurrent modification exceptions
109: return new FixedMap(getKeys(), getValues());
110: }
111:
112: public final String[] getKeys() {
113: Map m = getContribution();
114: String[] result = new String[m.size()];
115: try {
116: return (String[]) m.keySet().toArray(result);
117: } catch (ClassCastException cce) {
118: System.err
119: .println("Only Strings are allowed as keys in the context map"); //NOI18N
120: throw cce;
121: }
122: }
123:
124: public final Object[] getValues() {
125: Map m = getContribution();
126: String[] result = new String[m.size()];
127: return m.values().toArray(result);
128: }
129: }
130:
131: /** Implementation of the Map interface over a pair of key/value arrays */
132: static class FixedMap implements Map {
133: private String[] keys;
134: private Object[] values;
135:
136: public FixedMap(String[] key, Object[] values) {
137: if (values.length != keys.length) {
138: throw new IllegalArgumentException(
139: "Keys and values must be same length arrays"); //NOI18N
140: }
141: assert !Arrays.asList(key).contains(IDENTITY) : "The key "
142: + IDENTITY
143: + " is reserved by the actions framework, "
144: + "and may not be used as a key"; //NOI18N
145: this .keys = keys;
146: this .values = values;
147: }
148:
149: public void clear() {
150: throw new UnsupportedOperationException();
151: }
152:
153: private Integer identity = null;
154:
155: Integer getIdentity() {
156: if (identity == null) {
157: identity = new Integer(identityOf(keys));
158: }
159: return identity;
160: }
161:
162: public boolean containsKey(Object key) {
163: if (IDENTITY.equals(key)) {
164: return true;
165: }
166: return Arrays.asList(keys).contains(key);
167: }
168:
169: public boolean containsValue(Object value) {
170: boolean result = Arrays.asList(values).contains(value);
171: if (!result && getIdentity().equals(value)) {
172: result = true;
173: }
174: return result;
175: }
176:
177: /** Not implemented - returns the empty set */
178: public java.util.Set entrySet() {
179: return Collections.EMPTY_SET;
180: }
181:
182: public Object get(Object key) {
183: if (IDENTITY.equals(key)) {
184: return getIdentity();
185: }
186: int i = Arrays.asList(keys).indexOf(key);
187: if (i != -1) {
188: return values[i];
189: }
190: return null;
191: }
192:
193: /** Will return the real values length, not including the
194: * identity key */
195: public boolean isEmpty() {
196: return values.length == 0;
197: }
198:
199: public java.util.Set keySet() {
200: Set result = new HashSet(Arrays.asList(keys));
201: result.add(IDENTITY);
202: return result;
203: }
204:
205: public Object put(Object key, Object value) {
206: throw new UnsupportedOperationException();
207: }
208:
209: public void putAll(Map t) {
210: throw new UnsupportedOperationException();
211: }
212:
213: public Object remove(Object key) {
214: throw new UnsupportedOperationException();
215: }
216:
217: public int size() {
218: //add 1 for identity
219: return keys.length == 0 ? 0 : keys.length + 1;
220: }
221:
222: public java.util.Collection values() {
223: return new HashSet(Arrays.asList(values));
224: }
225:
226: }
227:
228: /** Utility method that returns a String array of the keys in a map
229: * (only called if we're not dealing with a fixed map).
230: */
231: private static String[] keys(Map m) {
232: Set keys = new HashSet(m.keySet());
233: keys.remove(IDENTITY);
234: String[] result = new String[keys.size()];
235: return (String[]) keys.toArray(result);
236: }
237:
238: /** Returns a unique hash of an array of strings. Used for composing an
239: * identity value for multiple maps, and by FixedMap to compute its identity
240: * value */
241: private static int identityOf(String[] st) {
242: if (st.length == 0) {
243: return 0;
244: } else {
245: int result = 0;
246: for (int i = 0; i < st.length; i++) {
247: result ^= st.hashCode();
248: }
249: return result;
250: }
251: }
252:
253: /** A map that proxies an array of maps. Note that due to its nature,
254: * it is possible to have duplicate keys - filtering them doesn't make
255: * sense for performance reasons. */
256: static class ProxyMap implements Map {
257: private Map[] maps;
258:
259: public ProxyMap(Map[] maps) {
260: this .maps = maps;
261: }
262:
263: private Integer identity = null;
264:
265: private Integer getIdentity() {
266: if (identity == null) {
267: computeIdentity();
268: }
269: return identity;
270: }
271:
272: private void computeIdentity() {
273: int result = 0;
274: for (int i = 0; i < maps.length; i++) {
275: //Some minor optimizations for known types
276: if (maps[i] instanceof FixedMap) {
277: result ^= ((FixedMap) maps[i]).getIdentity()
278: .intValue();
279: } else if (maps[i] instanceof ProxyMap) {
280: result ^= ((ProxyMap) maps[i]).getIdentity()
281: .intValue();
282: } else {
283: result ^= identityOf(keys(maps[i]));
284: }
285: }
286: identity = new Integer(result);
287: }
288:
289: //XXX more effcient may be to simply compose a real hashmap on any
290: //call that accesses one of the proxied maps, and then proxy that
291: //for the rest of the ProxyMap's lifetime. Would also cure the
292: //duplicate problem.
293:
294: public void clear() {
295: throw new UnsupportedOperationException();
296: }
297:
298: public boolean containsKey(Object key) {
299: if (IDENTITY.equals(key)) {
300: return true;
301: }
302: boolean result = false;
303: for (int i = 0; i < maps.length; i++) {
304: result |= maps[i].containsKey(key);
305: if (result) {
306: break;
307: }
308: }
309: return result;
310: }
311:
312: public boolean containsValue(Object value) {
313: boolean result = false;
314: for (int i = 0; i < maps.length; i++) {
315: result |= maps[i].containsValue(value);
316: if (result) {
317: break;
318: }
319: }
320: if (!result && value.equals(getIdentity())) {
321: return true;
322: }
323: return result;
324: }
325:
326: public java.util.Set entrySet() {
327: return Collections.EMPTY_SET;
328: }
329:
330: public Object get(Object key) {
331: if (IDENTITY.equals(key)) {
332: return getIdentity();
333: }
334: Object result = null;
335: for (int i = 0; i < maps.length; i++) {
336: result = maps[i].get(key);
337: if (result != null) {
338: break;
339: }
340: }
341: return result;
342: }
343:
344: /** Note this does not account for the IDENTITY key or it would
345: * always be true */
346: public boolean isEmpty() {
347: boolean result = maps.length == 0;
348: if (!result) {
349: for (int i = 0; i < maps.length; i++) {
350: result &= maps[i].isEmpty();
351: if (result) {
352: break;
353: }
354: }
355: }
356: return result;
357: }
358:
359: public Set keySet() {
360: Set result = new HashSet();
361: for (int i = 0; i < maps.length; i++) {
362: result.addAll(maps[i].keySet());
363: }
364: result.add(IDENTITY);
365: return result;
366: }
367:
368: public Object put(Object key, Object value) {
369: throw new UnsupportedOperationException();
370: }
371:
372: public void putAll(Map t) {
373: throw new UnsupportedOperationException();
374: }
375:
376: public Object remove(Object key) {
377: throw new UnsupportedOperationException();
378: }
379:
380: public int size() {
381: int result = 0;
382: if (maps.length > 0) {
383: for (int i = 0; i < maps.length; i++) {
384: result += maps[i].size();
385: }
386: }
387: //Add one for the identity key.
388: return result + 1;
389: }
390:
391: public java.util.Collection values() {
392: if (maps.length == 0) {
393: return Collections.EMPTY_SET;
394: } else {
395: Set result = new HashSet();
396: for (int i = 0; i < maps.length; i++) {
397: result.addAll(maps[i].values());
398: }
399: result.add(getIdentity());
400: return result;
401: }
402: }
403: }
404: }
|