001: // Copyright 2006, 2007 The Apache Software Foundation
002: //
003: // Licensed under the Apache License, Version 2.0 (the "License");
004: // you may not use this file except in compliance with the License.
005: // You may obtain a copy of the License at
006: //
007: // http://www.apache.org/licenses/LICENSE-2.0
008: //
009: // Unless required by applicable law or agreed to in writing, software
010: // distributed under the License is distributed on an "AS IS" BASIS,
011: // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012: // See the License for the specific language governing permissions and
013: // limitations under the License.
014:
015: package org.apache.tapestry.internal.services;
016:
017: import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newCaseInsensitiveMap;
018: import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newList;
019: import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newMap;
020:
021: import java.util.Collection;
022: import java.util.List;
023: import java.util.Map;
024:
025: import org.apache.tapestry.internal.InternalConstants;
026: import org.apache.tapestry.internal.events.InvalidationListener;
027: import org.apache.tapestry.ioc.annotations.Inject;
028: import org.apache.tapestry.ioc.annotations.Symbol;
029: import org.apache.tapestry.ioc.internal.util.ConcurrentBarrier;
030: import org.apache.tapestry.ioc.internal.util.Invokable;
031: import org.apache.tapestry.services.ComponentClassResolver;
032: import org.apache.tapestry.services.LibraryMapping;
033:
034: public class ComponentClassResolverImpl implements
035: ComponentClassResolver, InvalidationListener {
036: private static final String MIXINS_SUBPACKAGE = "mixins";
037:
038: private static final String COMPONENTS_SUBPACKAGE = "components";
039:
040: private static final String PAGES_SUBPACKAGE = "pages";
041:
042: private final ComponentInstantiatorSource _componentInstantiatorSource;
043:
044: private final ClassNameLocator _classNameLocator;
045:
046: private final String _appRootPackage;
047:
048: // Map from folder name to a list of root package names.
049: // The key does not begin or end with a slash.
050:
051: private final Map<String, List<String>> _mappings = newCaseInsensitiveMap();
052:
053: // Flag indicating that the maps have been cleared following an invalidation
054: // and need to be rebuilt. The flag and the four maps below are not synchronized
055: // because they are only modified inside a synchronized block. That should be strong enough ...
056: // and changes made will become "visible" at the end of the synchronized block. Because of the
057: // structure of Tapestry, there should not be any reader threads while the write thread
058: // is operating.
059:
060: private boolean _needsRebuild = true;
061:
062: private final Map<String, String> _pageToClassName = newCaseInsensitiveMap();
063:
064: private final Map<String, String> _componentToClassName = newCaseInsensitiveMap();
065:
066: private final Map<String, String> _mixinToClassName = newCaseInsensitiveMap();
067:
068: /** This one is case sensitive, since class names do always have a particular case. */
069: private final Map<String, String> _pageClassNameToLogicalName = newMap();
070:
071: private final Map<String, String> _pageNameToCanonicalPageName = newCaseInsensitiveMap();
072:
073: private final ConcurrentBarrier _barrier = new ConcurrentBarrier();
074:
075: public ComponentClassResolverImpl(
076: ComponentInstantiatorSource componentInstantiatorSource,
077: ClassNameLocator classNameLocator,
078:
079: @Inject
080: @Symbol(InternalConstants.TAPESTRY_APP_PACKAGE_PARAM)
081: String appRootPackage,
082:
083: Collection<LibraryMapping> mappings) {
084: _componentInstantiatorSource = componentInstantiatorSource;
085: _classNameLocator = classNameLocator;
086:
087: _appRootPackage = appRootPackage;
088:
089: addPackagesToInstantiatorSource(_appRootPackage);
090:
091: for (LibraryMapping mapping : mappings) {
092: String prefix = mapping.getPathPrefix();
093:
094: // TODO: Check that prefix is well formed (no leading or trailing slash)
095:
096: String rootPackage = mapping.getRootPackage();
097:
098: List<String> packages = _mappings.get(prefix);
099:
100: if (packages == null) {
101: packages = newList();
102: _mappings.put(prefix, packages);
103: }
104:
105: packages.add(rootPackage);
106:
107: // These packages, which will contain classes subject to class transformation,
108: // must be registered with the component instantiator (which is responsible
109: // for transformation).
110:
111: addPackagesToInstantiatorSource(rootPackage);
112: }
113:
114: }
115:
116: private void addPackagesToInstantiatorSource(String rootPackage) {
117: _componentInstantiatorSource.addPackage(rootPackage + ".pages");
118: _componentInstantiatorSource.addPackage(rootPackage
119: + ".components");
120: _componentInstantiatorSource
121: .addPackage(rootPackage + ".mixins");
122: _componentInstantiatorSource.addPackage(rootPackage + ".base");
123: }
124:
125: /** When the class loader is invalidated, clear any cached page names or component types. */
126: public synchronized void objectWasInvalidated() {
127: _barrier.withWrite(new Runnable() {
128: public void run() {
129: _needsRebuild = true;
130:
131: _pageToClassName.clear();
132: _componentToClassName.clear();
133: _mixinToClassName.clear();
134: _pageClassNameToLogicalName.clear();
135: _pageNameToCanonicalPageName.clear();
136: }
137: });
138:
139: }
140:
141: private void rebuild() {
142: if (!_needsRebuild)
143: return;
144:
145: _barrier.withWrite(new Runnable() {
146: public void run() {
147: rebuild("", _appRootPackage);
148:
149: for (String prefix : _mappings.keySet()) {
150: List<String> packages = _mappings.get(prefix);
151:
152: String folder = prefix + "/";
153:
154: for (String packageName : packages)
155: rebuild(folder, packageName);
156: }
157:
158: _needsRebuild = false;
159: }
160: });
161: }
162:
163: private void rebuild(String pathPrefix, String rootPackage) {
164: fillNameToClassNameMap(pathPrefix, rootPackage,
165: PAGES_SUBPACKAGE, _pageToClassName);
166: fillNameToClassNameMap(pathPrefix, rootPackage,
167: COMPONENTS_SUBPACKAGE, _componentToClassName);
168: fillNameToClassNameMap(pathPrefix, rootPackage,
169: MIXINS_SUBPACKAGE, _mixinToClassName);
170: }
171:
172: private void fillNameToClassNameMap(String pathPrefix,
173: String rootPackage, String subPackage,
174: Map<String, String> logicalNameToClassName) {
175: String searchPackage = rootPackage + "." + subPackage;
176: boolean isPage = subPackage.equals(PAGES_SUBPACKAGE);
177:
178: Collection<String> classNames = _classNameLocator
179: .locateClassNames(searchPackage);
180:
181: int startPos = searchPackage.length() + 1;
182:
183: for (String name : classNames) {
184: String logicalName = toLogicalName(name, pathPrefix,
185: startPos);
186:
187: if (isPage) {
188: _pageClassNameToLogicalName.put(name, logicalName);
189: _pageNameToCanonicalPageName.put(logicalName,
190: logicalName);
191: }
192:
193: logicalNameToClassName.put(logicalName, name);
194: }
195: }
196:
197: /**
198: * Converts a fully qualified class name to a logical name
199: *
200: * @param className
201: * fully qualified class name
202: * @param pathPrefix
203: * prefix to be placed on the logical name (to identify the library from in which the
204: * class lives)
205: * @param startPos
206: * start position within the class name to extract the logical name (i.e., after the
207: * final '.' in "rootpackage.pages.").
208: * @return a short logical name in folder format ('.' replaced with '/')
209: */
210: private String toLogicalName(String className, String pathPrefix,
211: int startPos) {
212: String[] terms = className.substring(startPos).split("\\.");
213: StringBuilder builder = new StringBuilder(pathPrefix);
214: String sep = "";
215:
216: String logicalName = terms[terms.length - 1];
217:
218: for (int i = 0; i < terms.length - 1; i++) {
219:
220: String term = terms[i];
221:
222: builder.append(sep);
223: builder.append(term);
224:
225: sep = "/";
226:
227: logicalName = stripTerm(term, logicalName);
228: }
229:
230: // The problem here is that you can eventually end up with the empty string.
231:
232: assert logicalName.length() > 0;
233:
234: builder.append(sep);
235: builder.append(logicalName);
236:
237: return builder.toString();
238: }
239:
240: private String stripTerm(String term, String logicalName) {
241: if (isCaselessPrefix(term, logicalName)) {
242: logicalName = logicalName.substring(term.length());
243: }
244:
245: if (isCaselessSuffix(term, logicalName)) {
246: logicalName = logicalName.substring(0, logicalName.length()
247: - term.length());
248: }
249:
250: return logicalName;
251: }
252:
253: private boolean isCaselessPrefix(String prefix, String string) {
254: return string
255: .regionMatches(true, 0, prefix, 0, prefix.length());
256: }
257:
258: private boolean isCaselessSuffix(String suffix, String string) {
259: return string.regionMatches(true, string.length()
260: - suffix.length(), suffix, 0, suffix.length());
261: }
262:
263: public String resolvePageNameToClassName(final String pageName) {
264: return _barrier.withRead(new Invokable<String>() {
265: public String invoke() {
266: String result = locate(pageName, _pageToClassName);
267:
268: if (result == null)
269: throw new IllegalArgumentException(ServicesMessages
270: .couldNotResolvePageName(pageName,
271: _pageToClassName.keySet()));
272:
273: return result;
274: }
275: });
276:
277: }
278:
279: public boolean isPageName(final String pageName) {
280: return _barrier.withRead(new Invokable<Boolean>() {
281: public Boolean invoke() {
282: return locate(pageName, _pageToClassName) != null;
283: }
284: });
285: }
286:
287: public String resolveComponentTypeToClassName(
288: final String componentType) {
289: return _barrier.withRead(new Invokable<String>() {
290: public String invoke() {
291: String result = locate(componentType,
292: _componentToClassName);
293:
294: if (result == null)
295: throw new IllegalArgumentException(ServicesMessages
296: .couldNotResolveComponentType(
297: componentType,
298: _componentToClassName.keySet()));
299:
300: return result;
301: }
302: });
303: }
304:
305: public String resolveMixinTypeToClassName(final String mixinType) {
306: return _barrier.withRead(new Invokable<String>() {
307: public String invoke() {
308: String result = locate(mixinType, _mixinToClassName);
309:
310: if (result == null)
311: throw new IllegalArgumentException(ServicesMessages
312: .couldNotResolveMixinType(mixinType,
313: _mixinToClassName.keySet()));
314:
315: return result;
316: }
317: });
318: }
319:
320: /**
321: * Locates a class name within the provided map, given its logical name. If not found naturally,
322: * a search inside the "core" library is included.
323: *
324: * @param logicalName
325: * name to search for
326: * @param logicalNameToClassName
327: * mapping from logical name to class name
328: * @return the located class name or null
329: */
330: private String locate(String logicalName,
331: Map<String, String> logicalNameToClassName) {
332: rebuild();
333:
334: String result = logicalNameToClassName.get(logicalName);
335:
336: // If not found, see if it exists under the core package. In this way,
337: // anything in core is "inherited" (but overridable) by the application.
338:
339: if (result == null)
340: result = logicalNameToClassName.get("core/" + logicalName);
341:
342: return result;
343: }
344:
345: public String resolvePageClassNameToPageName(
346: final String pageClassName) {
347: return _barrier.withRead(new Invokable<String>() {
348: public String invoke() {
349: rebuild();
350:
351: String result = _pageClassNameToLogicalName
352: .get(pageClassName);
353:
354: if (result == null)
355: throw new IllegalArgumentException(ServicesMessages
356: .pageNameUnresolved(pageClassName));
357:
358: return result;
359: }
360: });
361: }
362:
363: public String canonicalizePageName(final String pageName) {
364: return _barrier.withRead(new Invokable<String>() {
365: public String invoke() {
366: String result = locate(pageName,
367: _pageNameToCanonicalPageName);
368:
369: if (result == null)
370: throw new IllegalArgumentException(ServicesMessages
371: .couldNotCanonicalizePageName(pageName,
372: _pageNameToCanonicalPageName
373: .keySet()));
374:
375: return result;
376: }
377: });
378: }
379:
380: }
|