001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.editor;
043:
044: import java.util.HashMap;
045: import java.util.ArrayList;
046:
047: /**
048: * Immutable and 'interned' wrapper holding
049: * an array of the contexts starting
050: * with the original context in which the token is defined
051: * and ending with the target context from which the token
052: * is being returned.
053: * It is final and has no public constructor.
054: * The only entrypoint is through the <tt>get()</tt> method.
055: * It's guaranteed that the two context-paths containing
056: * the same contexts in the same order are the same objects
057: * and the equal-operator can be used instead of calling <tt>equals()</tt>.
058: *
059: * @author Miloslav Metelka
060: * @version 1.00
061: */
062:
063: public final class TokenContextPath {
064:
065: /** Cache containing [ArrayMatcher, TokenContextPath] pairs. */
066: private static final HashMap registry = new HashMap(199);
067:
068: /** Contexts contained in this context-path. */
069: private TokenContext[] contexts;
070:
071: /** Path for context-array without the last member. */
072: private TokenContextPath parent;
073:
074: /** Path corresponding to the first context in the context-array. */
075: private TokenContextPath base;
076:
077: /** Full name-prefix consisting of the prefixes
078: * of all the contexts in the path.
079: */
080: private String namePrefix;
081:
082: /** Map holding [token-name, prefixed-token-name] pairs. */
083: private final HashMap tokenNameCache = new HashMap();
084:
085: /** Map holding [start-path-replacement, replaced-path] */
086: private final HashMap replaceStartCache = new HashMap();
087:
088: /** Get the context-path for non-empty array of the contexts. */
089: static synchronized TokenContextPath get(TokenContext[] contexts) {
090: if (contexts == null || contexts.length == 0) {
091: throw new IllegalArgumentException(
092: "Contexts must be valid and non-empty."); // NOI18N
093: }
094:
095: ArrayMatcher am = new ArrayMatcher(contexts);
096: TokenContextPath path = (TokenContextPath) registry.get(am);
097: if (path == null) {
098: path = new TokenContextPath(contexts);
099: registry.put(am, path);
100: }
101:
102: return path;
103: }
104:
105: /** Construction from outside prohibited. */
106: private TokenContextPath(TokenContext[] contexts) {
107: this .contexts = contexts;
108:
109: if (contexts.length == 1) {
110: base = this ; // it's base for itself
111: }
112: }
113:
114: /** Retrieve the contexts of this context-path. The contexts
115: * of the context-array must NOT be modified in any way.
116: */
117: public TokenContext[] getContexts() {
118: return contexts;
119: }
120:
121: /** Get the length of the path returning the length of the contexts array. */
122: public int length() {
123: return contexts.length;
124: }
125:
126: /** Get parent context-path that consists of all the contexts
127: * of this path except the last one.
128: */
129: public TokenContextPath getParent() {
130: if (parent == null && contexts.length > 1) {
131: TokenContext[] parentContexts = new TokenContext[contexts.length - 1];
132: System.arraycopy(contexts, 0, parentContexts, 0,
133: contexts.length - 1);
134:
135: parent = get(parentContexts);
136: }
137:
138: return parent;
139: }
140:
141: /** Get the base path which corresponds to only the first context
142: * in the context-array. The base path can be used for fast checking
143: * of the origin path of the token.
144: */
145: public TokenContextPath getBase() {
146: if (base == null) {
147: base = getParent().getBase();
148: }
149:
150: return base;
151: }
152:
153: /** Does this path contain the given path. It corresponds
154: * to the situation when the contexts of the given path
155: * are at the begining of this path.
156: */
157: public boolean contains(TokenContextPath tcp) {
158: if (tcp == this ) {
159: return true;
160:
161: } else if (contexts.length > 1) {
162: return getParent().contains(tcp);
163:
164: } else {
165: return false;
166: }
167: }
168:
169: /** Get the path which has the initial part of the path
170: * (usually only the base path) replaced by the given path. The length
171: * of the replaced part of the path is the same as the length
172: * of the path that will replace it.
173: * For better performance the method caches the [byPath, result-path]
174: * in hashmap.
175: * @param byPath path that will replace the initial portion
176: * of this path. The length of the portion is the same as the length
177: * of this parameter.
178: * @return the path with the initial part being replaced.
179: */
180: public TokenContextPath replaceStart(TokenContextPath byPath) {
181: // Check whether byPath isn't longer than this path
182: if (contexts.length < byPath.contexts.length) {
183: throw new IllegalArgumentException("byPath=" + byPath
184: + " is too long."); // NOI18N
185: }
186:
187: synchronized (replaceStartCache) {
188: TokenContextPath ret = (TokenContextPath) replaceStartCache
189: .get(byPath);
190: if (ret == null) {
191: TokenContext[] targetContexts = (TokenContext[]) contexts
192: .clone();
193: for (int i = byPath.contexts.length - 1; i >= 0; i--) {
194: targetContexts[i] = byPath.contexts[i];
195: }
196: ret = get(targetContexts);
197: replaceStartCache.put(byPath, ret);
198: }
199:
200: return ret;
201: }
202: }
203:
204: /** Get the prefix that this context adds to the name of its tokens. */
205: public String getNamePrefix() {
206: if (namePrefix == null) {
207: if (contexts.length == 1) {
208: namePrefix = contexts[0].getNamePrefix();
209:
210: } else { // path has more contexts
211: namePrefix = (contexts[contexts.length - 1]
212: .getNamePrefix() + getParent().getNamePrefix())
213: .intern();
214: }
215: }
216:
217: return namePrefix;
218: }
219:
220: /** Get the token-name with the name-prefix of this context-path.
221: * It merges the token-name with the name-prefix of this context-path
222: * but it does it without creating a new object.
223: */
224: public String getFullTokenName(TokenCategory tokenIDOrCategory) {
225: String tokenName = tokenIDOrCategory.getName();
226: String fullName;
227: synchronized (tokenNameCache) {
228: fullName = (String) tokenNameCache.get(tokenName);
229: if (fullName == null) {
230: fullName = (getNamePrefix() + tokenName).intern();
231: tokenNameCache.put(tokenName, fullName);
232: }
233: }
234:
235: return fullName;
236: }
237:
238: public String toString() {
239: StringBuffer sb = new StringBuffer("|"); // NOI18N
240: for (int i = 0; i < contexts.length; i++) {
241: String shortName = contexts[i].getClass().getName();
242: shortName = shortName
243: .substring(shortName.lastIndexOf('.') + 1);
244:
245: sb.append('<');
246: sb.append(shortName);
247: sb.append('>');
248: }
249: sb.append('|');
250:
251: return sb.toString();
252: }
253:
254: private static final class ArrayMatcher {
255:
256: /** Cached hash-code */
257: private int hash;
258:
259: private TokenContext[] contexts;
260:
261: ArrayMatcher(TokenContext[] contexts) {
262: this .contexts = contexts;
263: }
264:
265: public int hashCode() {
266: int h = hash;
267: if (h == 0) {
268: for (int i = contexts.length - 1; i >= 0; i--) {
269: h = h * 31 + contexts[i].hashCode(); // !!!
270: }
271: hash = h;
272: }
273:
274: return h;
275: }
276:
277: public boolean equals(Object o) {
278: if (this == o) {
279: return true;
280: }
281:
282: if (o instanceof ArrayMatcher) {
283: ArrayMatcher am = (ArrayMatcher) o;
284: if (contexts.length == am.contexts.length) {
285: for (int i = contexts.length - 1; i >= 0; i--) {
286: if (!contexts[i].equals(am.contexts[i])) {
287: return false;
288: }
289: }
290: return true;
291: }
292: }
293:
294: return false;
295: }
296:
297: }
298:
299: }
|