org.cast.cwm.xml.component.XmlComponent.java Source code

Java tutorial

Introduction

Here is the source code for org.cast.cwm.xml.component.XmlComponent.java

Source

/*
 * Copyright 2011-2016 CAST, Inc.
 *
 * This file is part of the CAST Wicket Modules:
 * see <http://code.google.com/p/cast-wicket-modules>.
 *
 * The CAST Wicket Modules are free software: you can redistribute and/or
 * modify them under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * The CAST Wicket Modules are distributed in the hope that they will be
 * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this software.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.cast.cwm.xml.component;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import lombok.Getter;
import lombok.Setter;

import org.apache.wicket.AttributeModifier;
import org.apache.wicket.Component;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.behavior.Behavior;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.IMarkupCacheKeyProvider;
import org.apache.wicket.markup.IMarkupResourceStreamProvider;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.markup.parser.XmlTag.TagType;
import org.apache.wicket.util.resource.IResourceStream;
import org.apache.wicket.util.resource.StringResourceStream;
import org.cast.cwm.xml.ICacheableModel;
import org.cast.cwm.xml.IXmlPointer;
import org.cast.cwm.xml.TransformResult;
import org.cast.cwm.xml.XmlSection;
import org.cast.cwm.xml.service.IXmlService;
import org.cast.cwm.xml.transform.TransformParameters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.google.inject.Inject;

/**
 * A Wicket Component that displays HTML generated from a section of an XML file.
 * @author bgoldowsky
 *
 */
public class XmlComponent extends Panel implements IMarkupResourceStreamProvider, IMarkupCacheKeyProvider {

    private static final Logger log = LoggerFactory.getLogger(XmlComponent.class);
    private static final long serialVersionUID = 1L;
    private static final String WICKET_NS = "http://wicket.apache.org";

    private boolean previouslyPopulated; // Has this been rendered yet?

    @Getter
    @Setter
    private boolean updateable = false; // Will this XmlComponent ever be redrawn via Ajax?
    private String transformName;

    @Getter
    @Setter
    private TransformParameters transformParameters = null;

    @Inject
    protected IXmlService xmlService;

    public XmlComponent(String id, ICacheableModel<? extends IXmlPointer> secMod, String transformName) {
        super(id, secMod);
        this.transformName = transformName;
        this.setEscapeModelStrings(false);
    }

    @SuppressWarnings("unchecked")
    public ICacheableModel<? extends IXmlPointer> getModel() {
        return (ICacheableModel<? extends IXmlPointer>) getDefaultModel();
    }

    @Override
    protected void onBeforeRender() {
        if (updateable || !previouslyPopulated) {
            addDynamicComponents();
            previouslyPopulated = true;
        }
        super.onBeforeRender();
    }

    public void setTransformParameter(String key, Object value) {
        if (transformParameters == null)
            transformParameters = new TransformParameters();
        transformParameters.put(key, value);
    }

    public boolean isEmpty() {
        TransformResult res = xmlService.getTransformed(getModel(), transformName, transformParameters);
        return res.getElement() == null;
    }

    /** 
     * <p>
     * Find all elements in this component's markup that have a wicket:id, and attach a dynamic component for them.
     * This supports nested Wicket Components.  A container can add components to itself elsewhere and those components 
     * will not be overridden/replaced by this method.  
     * 
     */
    protected void addDynamicComponents() {
        TransformResult transformResult = xmlService.getTransformed(getModel(), transformName, transformParameters);
        Element dom = transformResult.getElement();
        NodeList componentNodes = xmlService.getWicketNodes(dom, true);
        Map<Element, Component> componentMap = new HashMap<Element, Component>(); // Mapping of Nodes to Components; used for nesting
        final Set<String> wicketIds = new HashSet<String>();

        for (int i = 0; i < componentNodes.getLength(); i++) {

            Element e = (Element) componentNodes.item(i);

            String id = e.getAttributeNS(WICKET_NS, "id");

            // Traverse up through this node's parents.  If a parent
            // maps to a wicket component, then that must be a MarkupContainer
            // for the current component.
            MarkupContainer container = null;
            Node parent = e.getParentNode();
            while (container == null && parent != null) {
                container = (MarkupContainer) componentMap.get(parent);
                parent = parent.getParentNode();
            }

            // Check to see if a component is a direct child and if
            // one already exists with that id. If so, no need to 
            // regenerate, but mark that it's valid.
            if (updateable && container == null && get(id) != null) {
                wicketIds.add(id);
                continue;
            }

            // If no container, add directly to the document. If container exists, add 
            // the component only if one does not already exist with that wicket:id.  This allows
            // panels, subclasses, etc to add components to this markup and not be overridden
            // or duplicated.
            if (container == null) {
                log.trace("Adding Dynamic Component to Root Panel {}: {}.", id, e);
                wicketIds.add(id); // Valid child, for removing stale children later.
                Component c = getDynamicComponent(id, e);
                add(c);
                componentMap.put(e, c);

            } else {
                Component c = container.get(id);
                if (c == null) {
                    c = getDynamicComponent(id, e);
                    log.trace("Adding Dynamic Component ({}) to Container ({}).", id, container.getId());
                    container.add(c);
                    componentMap.put(e, c);
                } else {
                    componentMap.put(e, c);
                }
            }
        }

        // Remove any stale components that might have been added during previous renders.
        List<Component> removeList = new ArrayList<Component>(); // To be removed
        if (updateable) {
            for (int i = 0; i < size(); i++) {
                if (!wicketIds.contains(get(i).getId()))
                    removeList.add(get(i));
            }
            for (Component c : removeList)
                c.remove();
        }

    }

    /**
     * Determine what Wicket Component to insert into the XmlComponent based on the ID and XML Element.
     * Override this method to create the proper dynamic behavior for your application.
     * 
     * @param wicket:id of the sub-component
     * @param Post-transform XML Element that bears the wicket:id
     * @return a Component to add as a child of the XmlComponent
     */
    public Component getDynamicComponent(String wicketId, Element elt) {

        boolean isContainer = false;

        // Check to see if this element has any Wicket Children.
        NodeList childrenComponents = xmlService.getWicketNodes(elt, false);
        if (childrenComponents.getLength() > 0) {
            isContainer = true;
        }

        // If we found a child, return a container with debugging style.  Otherwise, a generic label.
        if (isContainer) {
            return new WebMarkupContainer(wicketId)
                    .add(AttributeModifier.replace("style", "border: 3px solid red"));
        } else {
            return new Label(wicketId, "[[[Dynamic component with ID " + wicketId + "]]]")
                    .add(AttributeModifier.replace("style", "border: 3px solid red"));
        }
    }

    /**
     * Get the String representation of the content, as HTML with Wicket IDs, from the XML Section.
     * @return
     */
    protected String getTransformedMarkup() {
        String content = xmlService.getTransformed(getModel(), transformName, transformParameters).getString();
        if (content == null)
            content = "";
        return "<wicket:panel>" + content + "</wicket:panel>";
    }

    /**
     * @see org.apache.wicket.Component#onComponentTag(org.apache.wicket.markup.ComponentTag)
     */
    @Override
    protected void onComponentTag(ComponentTag tag) {
        super.onComponentTag(tag);
        tag.setType(TagType.OPEN); // Ensure open/close tags.  Turns <span /> into <span></span>
    }

    @Override
    public IResourceStream getMarkupResourceStream(MarkupContainer container, Class<?> containerClass) {
        return new StringResourceStream(getTransformedMarkup());
    }

    @Override
    public String getCacheKey(MarkupContainer container, Class<?> containerClass) {
        return null;
    }

    /**
     * This class no longer assumes that the source object is an {@link XmlSection}.  Instead,
     * it uses an {@link IXmlPointer}, of which XmlSection is a subclass.
     * @return
     */
    @Deprecated
    public XmlSection getXmlSection() {
        Object obj = getDefaultModelObject();
        if (obj instanceof XmlSection)
            return (XmlSection) getDefaultModelObject();
        else
            return null;
    }

    /**
     * Will confirm that the given element has a valid wicket:id structure.  That 
     * is, each node that represents a wicket component must be unique to its wicket.
     * siblings.  This function will process all descendents of the given element.
     * 
     * @param elt
     */
    //   public static void confirmUniqueWicketIds(Element elt) {
    //      
    //      List<String> wicketIds = new ArrayList<String>();
    //      NodeList wicketChildren = getWicketNodes(elt, false);
    //      
    //      for (int i=0; i < wicketChildren.getLength(); i++) {
    //         Element child = (Element) wicketChildren.item(i);
    //         String id = child.getAttributeNS(WICKET_NS, "id");
    //         if (!wicketIds.contains(id)) {
    //            wicketIds.add(id);
    //         } else {
    //            int count = 0;
    //            String newId = id + "_" + count;
    //            while(wicketIds.contains(newId))
    //               newId = id + "_" + count++;
    //            wicketIds.add(newId);
    //            child.setAttributeNS(WICKET_NS, "id", newId);
    //            log.trace("Found Duplicate Wicket Id ({}) when processing DOM Tree.  Generated new Id ({})", id, newId);
    //         }
    //         confirmUniqueWicketIds(child);
    //      }
    //   }
    //   
    public static class AttributeRemover extends Behavior {

        private String[] atts;

        private static final long serialVersionUID = 1L;

        public AttributeRemover(String... atts) {
            this.atts = atts;
        }

        @Override
        public void onComponentTag(Component component, ComponentTag tag) {
            for (String at : atts)
                tag.getAttributes().remove(at);
        }
    }

    protected int getWidth(Element elt, int defaultValue) {
        try {
            return Integer.valueOf(elt.getAttributeNS(null, "width"));
        } catch (NumberFormatException e) {
            return defaultValue;
        }
    }

    protected int getHeight(Element elt, int defaultValue) {
        try {
            return Integer.valueOf(elt.getAttributeNS(null, "height"));
        } catch (NumberFormatException e) {
            return defaultValue;
        }
    }

}