001: /*
002: * $Id: Markup.java 461655 2006-07-30 12:37:49Z jdonnerstag $ $Revision: 461655 $
003: * $Date: 2006-07-30 14:37:49 +0200 (Sun, 30 Jul 2006) $
004: *
005: * ==============================================================================
006: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
007: * use this file except in compliance with the License. You may obtain a copy of
008: * the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing, software
013: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
014: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
015: * License for the specific language governing permissions and limitations under
016: * the License.
017: */
018: package wicket.markup;
019:
020: import java.util.HashMap;
021: import java.util.Iterator;
022: import java.util.Map;
023: import java.util.regex.Matcher;
024: import java.util.regex.Pattern;
025:
026: import org.apache.commons.logging.Log;
027: import org.apache.commons.logging.LogFactory;
028:
029: /**
030: * Holds markup as a resource (the stream that the markup came from) and a list
031: * of MarkupElements (the markup itself).
032: *
033: * @see MarkupElement
034: * @see ComponentTag
035: * @see wicket.markup.RawMarkup
036: *
037: * @author Jonathan Locke
038: * @author Juergen Donnerstag
039: */
040: public class Markup {
041: private static final Log log = LogFactory.getLog(Markup.class);
042:
043: /** Placeholder that indicates no markup */
044: public static final Markup NO_MARKUP = new Markup();
045:
046: /** The list of markup elements */
047: private MarkupFragment markup;
048:
049: /** The markup's resource stream for diagnostic purposes */
050: private MarkupResourceStream resource;
051:
052: /** If found in the markup, the <?xml ...?> string */
053: private String xmlDeclaration;
054:
055: /** The encoding as found in <?xml ... encoding="" ?>. Null, else */
056: private String encoding;
057:
058: /** Wicket namespace: <html xmlns:wicket="http://wicket.sourceforge.net"> */
059: private String wicketNamespace;
060:
061: /** == wicketNamespace + ":id" */
062: private String wicketId;
063:
064: /**
065: * Used at markup load time to maintain the current component path (not id)
066: * while adding markup elements to this Markup instance
067: */
068: private StringBuffer currentPath;
069:
070: /**
071: * A cache which maps (componentPath + id) to the componentTags index in the
072: * markup
073: */
074: private Map componentMap;
075:
076: /**
077: * Constructor
078: */
079: Markup() {
080: this .markup = new MarkupFragment(this );
081: setWicketNamespace(ComponentTag.DEFAULT_WICKET_NAMESPACE);
082: }
083:
084: /**
085: * Initialize the index where <head> can be found.
086: */
087: protected void initialize() {
088: // Reset
089: this .componentMap = null;
090:
091: if (markup != null) {
092: // HTML tags like <img> may not have a close tag. But because that
093: // can only be detected until later on in the sequential markup
094: // reading loop, we only can do it now.
095: StringBuffer componentPath = null;
096: for (int i = 0; i < this .markup.size(); i++) {
097: MarkupElement elem = this .markup.get(i);
098: if (elem instanceof ComponentTag) {
099: ComponentTag tag = (ComponentTag) elem;
100:
101: // Set the tags components path
102: componentPath = setComponentPathForTag(
103: componentPath, tag);
104:
105: // and add it to the local cache to be found fast if required
106: addToCache(i, tag);
107: }
108: }
109: }
110:
111: // The variable is only needed while adding markup elements.
112: // initialize() is invoked after all elements have been added.
113: this .currentPath = null;
114: }
115:
116: /**
117: * @return String representation of markup list
118: */
119: public String toString() {
120: if (resource != null) {
121: return resource.toString();
122: } else {
123: return "(unknown resource)";
124: }
125: }
126:
127: /**
128: * @return String representation of markup list
129: */
130: public String toDebugString() {
131: return this .markup.toString();
132: }
133:
134: /**
135: * For Wicket it would be sufficient for this method to be package
136: * protected. However to allow wicket-bench easy access to the information
137: * ...
138: *
139: * @param index
140: * Index into markup list
141: * @return Markup element
142: */
143: public MarkupElement get(final int index) {
144: return markup.get(index);
145: }
146:
147: /**
148: * Gets the resource that contains this markup
149: *
150: * @return The resource where this markup came from
151: */
152: MarkupResourceStream getResource() {
153: return resource;
154: }
155:
156: /**
157: * For Wicket it would be sufficient for this method to be package
158: * protected. However to allow wicket-bench easy access to the information
159: * ...
160: *
161: * @return Number of markup elements
162: */
163: public int size() {
164: return markup.size();
165: }
166:
167: /**
168: * Return the XML declaration string, in case if found in the markup.
169: *
170: * @return Null, if not found.
171: */
172: public String getXmlDeclaration() {
173: return xmlDeclaration;
174: }
175:
176: /**
177: * Gets the markup encoding. A markup encoding may be specified in a markup
178: * file with an XML encoding specifier of the form <?xml ...
179: * encoding="..." ?>.
180: *
181: * @return Encoding, or null if not found.
182: */
183: public String getEncoding() {
184: return encoding;
185: }
186:
187: /**
188: * Get the wicket namespace valid for this specific markup
189: *
190: * @return wicket namespace
191: */
192: public String getWicketNamespace() {
193: return this .wicketNamespace;
194: }
195:
196: /**
197: * Find the markup element index of the component with 'path'
198: *
199: * @param path
200: * The component path expression
201: * @param id
202: * The component's id to search for
203: * @return -1, if not found
204: */
205: public int findComponentIndex(final String path, final String id) {
206: if ((id == null) || (id.length() == 0)) {
207: throw new IllegalArgumentException(
208: "Parameter 'id' must not be null");
209: }
210:
211: // TODO Post 1.2: A component path e.g. "panel:label" does not match 1:1
212: // with the markup in case of ListView, where the path contains a number
213: // for each list item. E.g. list:0:label. What we currently do is simply
214: // remove the number from the path and hope that no user uses an integer
215: // for a component id. This is a hack only. A much better solution would
216: // delegate to the various components recursivly to search within there
217: // realm only for the components markup. ListItems could then simply
218: // do nothing and delegate to their parents.
219: String completePath = (path == null || path.length() == 0 ? id
220: : path + ":" + id);
221:
222: // s/:\d+//g
223: Pattern re = Pattern.compile(":\\d+");
224: Matcher matcher = re.matcher(completePath);
225: completePath = matcher.replaceAll("");
226:
227: // All component tags are registered with the cache
228: if (this .componentMap == null) {
229: // not found
230: return -1;
231: }
232:
233: final Integer value = (Integer) this .componentMap
234: .get(completePath);
235: if (value == null) {
236: // not found
237: return -1;
238: }
239:
240: // return the components position in the markup stream
241: return value.intValue();
242: }
243:
244: /**
245: * Sets encoding.
246: *
247: * @param encoding
248: * encoding
249: */
250: final void setEncoding(final String encoding) {
251: this .encoding = encoding;
252: }
253:
254: /**
255: * Sets wicketNamespace.
256: *
257: * @param wicketNamespace
258: * wicketNamespace
259: */
260: public final void setWicketNamespace(final String wicketNamespace) {
261: this .wicketNamespace = wicketNamespace;
262: this .wicketId = wicketNamespace + ":id";
263:
264: if (!ComponentTag.DEFAULT_WICKET_NAMESPACE
265: .equals(wicketNamespace)) {
266: log.info("You are using a non-standard component name: "
267: + wicketNamespace);
268: }
269: }
270:
271: /**
272: * Sets xmlDeclaration.
273: *
274: * @param xmlDeclaration
275: * xmlDeclaration
276: */
277: final void setXmlDeclaration(final String xmlDeclaration) {
278: this .xmlDeclaration = xmlDeclaration;
279: }
280:
281: /**
282: * Sets the resource stream associated with the markup. It is for diagnostic
283: * purposes only.
284: *
285: * @param resource
286: * the markup resource stream
287: */
288: final void setResource(final MarkupResourceStream resource) {
289: this .resource = resource;
290: }
291:
292: /**
293: * Add a MarkupElement
294: *
295: * @param markupElement
296: */
297: public final void addMarkupElement(final MarkupElement markupElement) {
298: this .markup.addMarkupElement(markupElement);
299: }
300:
301: /**
302: * Add a MarkupElement
303: *
304: * @param pos
305: * @param markupElement
306: */
307: final void addMarkupElement(final int pos,
308: final MarkupElement markupElement) {
309: this .markup.addMarkupElement(pos, markupElement);
310: }
311:
312: /**
313: * Add the tag to the local cache if open or open-close and if wicket:id is
314: * present
315: *
316: * @param index
317: * @param tag
318: */
319: private void addToCache(final int index, final ComponentTag tag) {
320: // Only if the tag has wicket:id="xx" and open or open-close
321: if ((tag.isOpen() || tag.isOpenClose())
322: && tag.getAttributes().containsKey(wicketId)) {
323: // Add the tag to the cache
324: if (this .componentMap == null) {
325: this .componentMap = new HashMap();
326: }
327:
328: final String key;
329: if (tag.getPath() != null) {
330: key = tag.getPath() + ":" + tag.getId();
331: } else {
332: key = tag.getId();
333: }
334: this .componentMap.put(key, new Integer(index));
335: }
336: }
337:
338: /**
339: * Set the components path within the markup and add the component tag to
340: * the local cache
341: *
342: * @param componentPath
343: * @param tag
344: * @return componentPath
345: */
346: private StringBuffer setComponentPathForTag(
347: final StringBuffer componentPath, final ComponentTag tag) {
348: // Only if the tag has wicket:id="xx" and open or open-close
349: if ((tag.isOpen() || tag.isOpenClose())
350: && tag.getAttributes().containsKey(wicketId)) {
351: // With open-close the path does not change. It can/will not have
352: // children. The same is true for HTML tags like <br> or <img>
353: // which might not have close tags.
354: if (tag.isOpenClose() || tag.hasNoCloseTag()) {
355: // Set the components path.
356: if ((this .currentPath != null)
357: && (this .currentPath.length() > 0)) {
358: tag.setPath(this .currentPath.toString());
359: }
360: } else {
361: // Set the components path.
362: if (this .currentPath == null) {
363: this .currentPath = new StringBuffer(100);
364: } else if (this .currentPath.length() > 0) {
365: tag.setPath(this .currentPath.toString());
366: this .currentPath.append(':');
367: }
368:
369: // .. and append the tags id to the component path for the
370: // children to come
371: this .currentPath.append(tag.getId());
372: }
373: } else if (tag.isClose() && (this .currentPath != null)) {
374: // For example <wicket:message> does not have an id
375: if ((tag.getOpenTag() == null)
376: || tag.getOpenTag().getAttributes().containsKey(
377: wicketId)) {
378: // Remove the last element from the component path
379: int index = this .currentPath.lastIndexOf(":");
380: if (index != -1) {
381: this .currentPath.setLength(index);
382: } else {
383: this .currentPath.setLength(0);
384: }
385: }
386: }
387:
388: return this .currentPath;
389: }
390:
391: /**
392: * Make all tags immutable and the list of elements unmodifable.
393: */
394: final void makeImmutable() {
395: this .markup.makeImmutable();
396:
397: // We assume all markup elements have now been added. It is
398: // now time to initialize all remaining variables based
399: // on the markup loaded, which could not be initialized
400: // earlier on.
401: initialize();
402: }
403:
404: /**
405: * Reset the markup to its defaults, except for the wicket namespace which
406: * remains unchanged.
407: */
408: final void reset() {
409: this .markup = new MarkupFragment(this );
410: this .resource = null;
411: this .xmlDeclaration = null;
412: this .encoding = null;
413: this .currentPath = null;
414: }
415:
416: /**
417: * Create an iterator for the component tags in the markup.
418: *
419: * @param startIndex
420: * The index to start with
421: * @param matchClass
422: * Iterate over elements matching the class
423: * @return ComponentTagIterator
424: */
425: public Iterator componentTagIterator(final int startIndex,
426: final Class matchClass) {
427: return this.markup.iterator(startIndex, matchClass);
428: }
429: }
|