/******************************************************************************* * Copyright (c) 2006 Sybase, 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: * Sybase, Inc. - initial API and implementation *******************************************************************************/ package org.eclipse.jst.pagedesigner.utils; import java.util.ArrayList; import java.util.Comparator; import javax.xml.namespace.QName; import org.eclipse.jst.jsf.core.internal.tld.CMUtil; import org.eclipse.jst.jsf.core.internal.tld.ITLDConstants; import org.eclipse.jst.pagedesigner.adapters.IBodyInfo; import org.eclipse.jst.pagedesigner.adapters.internal.BodyInfo; import org.eclipse.jst.pagedesigner.dom.DOMPosition; import org.eclipse.jst.pagedesigner.dom.DOMRefPosition; import org.eclipse.jst.pagedesigner.dom.DOMRefPosition2; import org.eclipse.jst.pagedesigner.dom.IDOMPosition; import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode; import org.eclipse.wst.xml.core.internal.provisional.document.IDOMText; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * This class helps location insertion position to inside correct body or doc * prefix. NOTE: this class only doing limited support on doc level position * validation. Element specific position validation will be done in other * places. * * @author mengbo */ public class BodyHelper { // bit flags used for child skipping. /** * Bit flag for empty text node */ public static final int EMPTY_TEXT = 1; /** * Bit flag for comment node */ public static final int COMMENT = 2; /** * Bit flag for HEAD node */ public static final int HEADER = 3; /** * * @param child * @return boolean */ private static boolean isSkippableChild(Node parent, Node child, int flag) { if ((flag & COMMENT) != 0 && child.getNodeType() == Node.COMMENT_NODE) return true; if ((flag & EMPTY_TEXT) != 0 && child instanceof IDOMText && ((IDOMText) child).isElementContentWhitespace()) return true; if ((flag & HEADER) != 0 && child.getNodeType() == Node.ELEMENT_NODE) { String uri = CMUtil.getElementNamespaceURI((Element) child); IBodyInfo parentInfo = getBodyInfo((IDOMNode) parent); if (parentInfo != null && parentInfo.isBodyHeader((IDOMNode) parent, uri, ((Element) child).getLocalName())) return true; } return false; } /** * check whether uri/tag should be header of any body container that is * ancester of the start node. * * @param start * @param uri * @param tag * @return IDOMNode */ public static IDOMNode findHeaderContainer(IDOMNode start, String uri, String tag) { while (start != null) { IBodyInfo designInfo = getBodyInfo(start); if (designInfo != null && designInfo.isBodyContainer(start)) { if (designInfo.isBodyHeader(start, uri, tag)) return start; } start = (IDOMNode) start.getParentNode(); } return null; } /** * find the closest body insertion point, to make it as deep as possible. * (Move into as more body as possible) * @param position * @return IDOMPosition */ public static IDOMPosition findBodyInsertLocation(IDOMPosition position) { // forward first. Node reference = position.getNextSiblingNode(); Node container = position.getContainerNode(); while (reference != null) { IBodyInfo info = getBodyInfo((IDOMNode) reference); if (info != null && info.isBodyContainer((IDOMNode) reference)) { // good, we find a body! position = new DOMPosition(reference, 0); return findBodyInsertLocation(position); } if (isSkippableChild(container, reference, EMPTY_TEXT | COMMENT | HEADER)) { reference = reference.getNextSibling(); continue; } break; } // backward reference = position.getPreviousSiblingNode(); while (reference != null) { IBodyInfo info = getBodyInfo((IDOMNode) reference); if (info != null && info.isBodyContainer((IDOMNode) reference)) { // good, we find a body! position = new DOMPosition(reference, reference.getChildNodes() .getLength()); return findBodyInsertLocation(position); } // XXX: not skip header here. So if there is some header with wrong // location, we will respect user. if (isSkippableChild(container, reference, EMPTY_TEXT | COMMENT)) { reference = reference.getPreviousSibling(); continue; } break; } // not find any body at same level as the insertion point. return position; } /** * The element type identified by "uri" and "tag" is going to be inserted * into the document. This method is used to adjust the insert position so * it can be put into correct body or header section. * @param uri * @param tag * @param position * @return IDOMPosition * */ public static IDOMPosition adjustInsertPosition(String uri, String tag, IDOMPosition position) { IDOMNode parent = (IDOMNode) position.getContainerNode(); IBodyInfo designInfo = getBodyInfo(parent); if (designInfo == null) { return position; // should not happen. } IDOMNode headerContainer = findHeaderContainer(parent, uri, tag); if (headerContainer == null) { // the new node is not header. if (shouldIgnoreAdjust(uri, tag)) { return position; } // new node is not body header. So should place inside the inner most // body. if (!designInfo.isBodyContainer(parent)) { return position; // it's parent is not body, so we suggest // it's parent already correctly located, and respect user's // choice. } // ok, we are inside some body, but we don't know whether we are in // the inner most body. // try to find a body container at same level and see whether we can // move into that body. return findBodyInsertLocation(position); } // good, we find a body container and the new node should be header // of it. Node child = headerContainer.getFirstChild(); // if parent is different from headerContainer, then // child!=referenceHolder[0] will always be true while (child != null) // && child != refNode) { Comparator comp = NodeLocationComparator.getInstance(); // Currently the comparator deels with tags like taglib and // loadbundle particularly, comparasion result 0 // means it didn't compare the tags. if (comp.compare(child, tag) < 0 || (comp.compare(child, tag) == 0 && isSkippableChild( headerContainer, child, COMMENT | EMPTY_TEXT | HEADER))) { child = child.getNextSibling(); } else { break; } } if (child != null) { return new DOMRefPosition(child, false); } return new DOMPosition(parent, parent.getChildNodes() .getLength()); } /** * Find the position to insert a header element into the specified parent. * * @param uri * @param tag * @param parent * @param ref */ public static void findHeaderInsertPosition(String uri, String tag, Node parent, Node[] ref) { Node child = parent.getFirstChild(); while (child != null) { Comparator comp = NodeLocationComparator.getInstance(); if (comp.compare(child, tag) < 0 || (comp.compare(child, tag) == 0 && isSkippableChild( parent, child, COMMENT | EMPTY_TEXT | HEADER))) { child = child.getNextSibling(); } else { break; } } ref[0] = child; return; } /** * @param position * @param body * @param defaultPrefix * @return the new dom position based on the insert. May return null if * insert fails. */ public static IDOMPosition insertBody(IDOMPosition position, QName body, String defaultPrefix) { IBodyInfo bodyInfo = getBodyInfo((IDOMNode) position.getContainerNode()); Node node = position.getContainerNode(); final Node originalContainer = node; final Node nextSibling = position.getNextSiblingNode(); // create the body element first. Document ownerDoc; if (node instanceof Document) { ownerDoc = (Document) node; } else { ownerDoc = node.getOwnerDocument(); } if (ownerDoc == null) { return null; // should not happen } final String prefix = JSPUtil.getOrCreatePrefix(((IDOMNode) node).getModel(), body.getNamespaceURI(), defaultPrefix); final Element ele = ownerDoc.createElement((prefix == null ? "" //$NON-NLS-1$ : (prefix + ":")) //$NON-NLS-1$ + body.getLocalPart()); // need to find out the insertion point while (node instanceof IDOMNode) { if (bodyInfo.isBodyContainer((IDOMNode) node)) { // ok, node is a body container. // we could create the new node as child of node and move all // node's none header children // as children of the new node. NodeList nl = node.getChildNodes(); ArrayList list = new ArrayList(); for (int i = 0; i < nl.getLength(); i++) { Node child = nl.item(i); if (isSkippableChild(node, child, HEADER | COMMENT | EMPTY_TEXT)) { continue; } list.add(nl.item(i)); } for (int i = 0; i < list.size(); i++) { ele.appendChild((Node) list.get(i)); } node.appendChild(ele); if (node == originalContainer) { if (nextSibling == null) { return new DOMRefPosition2(ele, true); } else if (nextSibling.getParentNode() == ele) { // next sibling is not in header part return new DOMRefPosition(nextSibling, false); } else { return new DOMPosition(ele, 0); } } return position; } node = node.getParentNode(); } // should not happen, because document and documentfragment node will // always be body node // so if reach here, means the position is not in document. return null; } /** * For certain special tags, do not following the "header"/"body" separation * and can't fit into the relocation process. * * @param uri * @param tag * @return true if tag is an element that should be moved in response to * body insert. */ public static boolean shouldIgnoreAdjust(String uri, String tag) { // FIXME: return (ITLDConstants.URI_HTML.equalsIgnoreCase(uri) && "script" //$NON-NLS-1$ .equalsIgnoreCase(tag)) || (ITLDConstants.URI_JSP.equals(uri)); } /** * @param node * @return the body info corresponding to node (should we use a node adapter?) */ public static IBodyInfo getBodyInfo(IDOMNode node) { // TODO: in the future, when bodyinfo is no longer singleton, we'll use // adapter mechanism. return BodyInfo.getInstance(); } }