/** * *************************************************************************** * Copyright (c) 2010 Qcadoo Limited * Project: Qcadoo Framework * Version: 1.4 * * This file is part of Qcadoo. * * Qcadoo is free software; you can redistribute it and/or modify * it 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. * * This program is distributed in the hope that it 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 program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * *************************************************************************** */ package com.qcadoo.view.internal.xml; import com.google.common.base.Preconditions; import com.qcadoo.localization.api.TranslationService; import com.qcadoo.model.api.DataDefinition; import com.qcadoo.model.api.DataDefinitionService; import com.qcadoo.security.api.SecurityRole; import com.qcadoo.security.api.SecurityRolesService; import com.qcadoo.view.internal.ComponentDefinition; import com.qcadoo.view.internal.ComponentOption; import com.qcadoo.view.internal.api.*; import com.qcadoo.view.internal.hooks.*; import com.qcadoo.view.internal.internal.ViewComponentsResolverImpl; import com.qcadoo.view.internal.internal.ViewDefinitionImpl; import com.qcadoo.view.internal.patterns.AbstractComponentPattern; import com.qcadoo.view.internal.ribbon.RibbonParserService; import com.qcadoo.view.internal.ribbon.model.InternalRibbon; import com.qcadoo.view.internal.ribbon.model.InternalRibbonActionItem; import com.qcadoo.view.internal.ribbon.model.InternalRibbonGroup; import org.apache.commons.lang3.ObjectUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @Service public final class ViewDefinitionParserImpl implements ViewDefinitionParser { private static final Logger LOG = LoggerFactory.getLogger(ViewDefinitionParserImpl.class); @Autowired private DataDefinitionService dataDefinitionService; @Autowired private InternalViewDefinitionService viewDefinitionService; @Autowired private ViewComponentsResolverImpl viewComponentsResolver; @Autowired private TranslationService translationService; @Autowired private ContextualHelpService contextualHelpService; @Autowired private SecurityRolesService securityRolesService; @Autowired private HookFactory hookFactory; @Autowired private ApplicationContext applicationContext; @Autowired private RibbonParserService ribbonService; private int currentIndexOrder; @Override public InternalViewDefinition parseViewXml(final Resource viewXml, final String pluginIdentifier) { try { return parse(viewXml.getInputStream(), pluginIdentifier); } catch (IOException e) { throw ViewDefinitionParserException.forFile(viewXml.getFilename(), "Error while reading view resource", e); } catch (ViewDefinitionParserNodeException e) { throw ViewDefinitionParserException.forFileAndNode(viewXml.getFilename(), e); } catch (Exception e) { throw ViewDefinitionParserException.forFile(viewXml.getFilename(), e); } } private InternalViewDefinition parse(final InputStream viewDefinitionInputStream, final String pluginIdentifier) throws ViewDefinitionParserNodeException { try { DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document document = documentBuilder.parse(viewDefinitionInputStream); Node root = document.getDocumentElement(); checkState("view".equals(root.getNodeName()), root, "Wrong root node '" + root.getNodeName() + "'"); return parseViewDefinition(root, pluginIdentifier); } catch (ParserConfigurationException e) { throw new IllegalStateException(e.getMessage(), e); } catch (SAXException e) { throw new IllegalStateException(e.getMessage(), e); } catch (IOException e) { throw new IllegalStateException(e.getMessage(), e); } } private InternalViewDefinition parseViewDefinition(final Node viewNode, final String pluginIdentifier) throws ViewDefinitionParserNodeException { currentIndexOrder = 1; String name = getStringAttribute(viewNode, "name"); Preconditions.checkState(name != null && !"".equals(name.trim()), "Name attribute cannot be empty"); LOG.info("Reading view " + name + " for plugin " + pluginIdentifier); boolean menuAccessible = getBooleanAttribute(viewNode, "menuAccessible", false); String windowWidthStr = getStringAttribute(viewNode, "windowWidth"); String windowHeightStr = getStringAttribute(viewNode, "windowHeight"); Integer windowWidth = null; Integer windowHeight = null; if (windowWidthStr != null) { windowWidth = Integer.parseInt(windowWidthStr); } if (windowHeightStr != null) { windowHeight = Integer.parseInt(windowHeightStr); } SecurityRole role = getAuthorizationRole(viewNode); DataDefinition dataDefinition = getDataDefinition(viewNode, pluginIdentifier); ViewDefinitionImpl viewDefinition = new ViewDefinitionImpl(name, pluginIdentifier, role, dataDefinition, menuAccessible, translationService); viewDefinition.setWindowDimmension(windowWidth, windowHeight); ComponentPattern root = null; NodeList childNodes = viewNode.getChildNodes(); for (int i = 0; i < childNodes.getLength(); i++) { Node child = childNodes.item(i); if (Node.ELEMENT_NODE != child.getNodeType()) { continue; } if ("component".equals(child.getNodeName())) { root = parseComponent(child, viewDefinition, null, pluginIdentifier); } else if ("hooks".equals(child.getNodeName())) { parseViewHooks(child, viewDefinition); } else { throw new ViewDefinitionParserNodeException(child, "Unknown node: " + child.getNodeName()); } } viewDefinition.addComponentPattern(root); viewDefinition.initialize(); viewDefinition.registerViews(viewDefinitionService); return viewDefinition; } private DataDefinition getDataDefinition(final Node viewNode, final String pluginIdentifier) { String modelName = getStringAttribute(viewNode, "modelName"); if (modelName != null) { // FIXME maku upgrade commons-lang to version in which defaultIfNull method is generic. // Explicit type casts are so awful :( String modelPluginIdentifier = (String) ObjectUtils.defaultIfNull(getStringAttribute(viewNode, "modelPlugin"), pluginIdentifier); return dataDefinitionService.get(modelPluginIdentifier, modelName); } return null; } public SecurityRole getAuthorizationRole(final Node node) throws ViewDefinitionParserNodeException { String authorizationRole = getStringAttribute(node, "defaultAuthorizationRole"); SecurityRole role; if (authorizationRole != null) { role = securityRolesService.getRoleByIdentifier(authorizationRole); if (role == null) { throw new ViewDefinitionParserNodeException(node, "no such role: '" + authorizationRole + "'"); } } else { role = securityRolesService.getRoleByIdentifier("ROLE_USER"); } return role; } @Override public Boolean getBooleanAttribute(final Node node, final String name, final boolean defaultValue) { Node attribute = getAttribute(node, name); if (attribute == null) { return defaultValue; } return Boolean.valueOf(attribute.getNodeValue()); } @Override public String getStringAttribute(final Node node, final String name) { Node attribute = getAttribute(node, name); if (attribute == null) { return null; } return attribute.getNodeValue(); } @Override public String getStringNodeContent(final Node node) { NodeList childNodes = node.getChildNodes(); StringBuilder contentSB = new StringBuilder(); for (int i = 0; i < childNodes.getLength(); i++) { Node child = childNodes.item(i); if (child.getNodeType() == Node.CDATA_SECTION_NODE || child.getNodeType() == Node.TEXT_NODE) { contentSB.append(child.getNodeValue()); } } return contentSB.toString().trim(); } @Override public Node getRootOfXmlDocument(final Resource xmlFile) { try { DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document document = documentBuilder.parse(xmlFile.getInputStream()); return document.getDocumentElement(); } catch (ParserConfigurationException e) { throw new IllegalStateException(e.getMessage(), e); } catch (SAXException e) { throw new IllegalStateException(e.getMessage(), e); } catch (IOException e) { throw new IllegalStateException(e.getMessage(), e); } } private Node getAttribute(final Node node, final String name) { if (node == null || node.getAttributes() == null) { return null; } return node.getAttributes().getNamedItem(name); } @Override public ComponentOption parseOption(final Node optionNode) { Map<String, String> attributes = new HashMap<String, String>(); NamedNodeMap attributesNodes = optionNode.getAttributes(); for (int i = 0; i < attributesNodes.getLength(); i++) { attributes.put(attributesNodes.item(i).getNodeName(), attributesNodes.item(i).getNodeValue()); } String type = getStringAttribute(optionNode, "type"); if (type == null) { type = getStringAttribute(optionNode, "xsi:type"); } return new ComponentOption(type, attributes); } public ComponentPattern parseComponent(final Node componentNode, final ViewDefinition viewDefinition, final ContainerPattern parent, final String pluginIdentifier) throws ViewDefinitionParserNodeException { String type = getStringAttribute(componentNode, "type"); if (parent == null && !("window".equals(type) || "tabWindow".equals(type))) { throw new ViewDefinitionParserNodeException(componentNode, "Unsupported component: " + type); } try { ComponentPattern component = viewComponentsResolver.getComponentInstance(type, getComponentDefinition(componentNode, parent, viewDefinition)); component.parse(componentNode, this); return component; } catch (IllegalStateException e) { throw new ViewDefinitionParserNodeException(componentNode, e); } } @Override public ComponentDefinition getComponentDefinition(final Node componentNode, final ContainerPattern parent, final ViewDefinition viewDefinition) { String name = getStringAttribute(componentNode, "name"); String fieldPath = getStringAttribute(componentNode, "field"); String sourceFieldPath = getStringAttribute(componentNode, "source"); String plugin = getStringAttribute(componentNode, "plugin"); String model = getStringAttribute(componentNode, "model"); Boolean useDto = getBooleanAttribute(componentNode, "useDto", false); DataDefinition customDataDefinition = null; if (model != null) { String modelPluginIdentifier = plugin == null ? viewDefinition.getPluginIdentifier() : plugin; customDataDefinition = dataDefinitionService.get(modelPluginIdentifier, model); } ComponentDefinition componentDefinition = new ComponentDefinition(); componentDefinition.setName(name); componentDefinition.setFieldPath(fieldPath); componentDefinition.setSourceFieldPath(sourceFieldPath); componentDefinition.setParent(parent); componentDefinition.setTranslationService(translationService); componentDefinition.setContextualHelpService(contextualHelpService); componentDefinition.setViewDefinition(viewDefinition); componentDefinition.setReference(getStringAttribute(componentNode, "reference")); componentDefinition.setDefaultVisible(getBooleanAttribute(componentNode, "defaultVisible", true)); componentDefinition.setHasLabel(getBooleanAttribute(componentNode, "hasLabel", true)); componentDefinition.setHasDescription(getBooleanAttribute(componentNode, "hasDescription", false)); componentDefinition.setDataDefinition(customDataDefinition); componentDefinition.setApplicationContext(applicationContext); componentDefinition.setUseDto(useDto); EnabledAttribute enabledAttribute = EnabledAttribute.TRUE; final String defaultEnabled = getStringAttribute(componentNode, "defaultEnabled"); if (defaultEnabled != null) { enabledAttribute = EnabledAttribute.parseString(defaultEnabled); } componentDefinition.setDefaultEnabled(EnabledAttribute.TRUE.equals(enabledAttribute)); componentDefinition.setPermanentlyDisabled(EnabledAttribute.NEVER.equals(enabledAttribute)); return componentDefinition; } @Override public ComponentPattern parseComponent(final Node componentNode, final ContainerPattern parent) throws ViewDefinitionParserNodeException { return parseComponent(componentNode, ((AbstractComponentPattern) parent).getViewDefinition(), parent, ((AbstractComponentPattern) parent).getViewDefinition().getPluginIdentifier()); } @Override public ViewEventListenerHook parseEventListener(final Node listenerNode) throws ViewDefinitionParserNodeException { try { return parseEventListenerHook(listenerNode); } catch (Exception e) { throw new ViewDefinitionParserNodeException(listenerNode, e); } } private void parseViewHooks(final Node hookNode, final ViewDefinitionImpl viewDefinition) throws ViewDefinitionParserNodeException { NodeList childNodes = hookNode.getChildNodes(); for (int i = 0; i < childNodes.getLength(); i++) { Node child = childNodes.item(i); if (Node.ELEMENT_NODE != child.getNodeType()) { continue; } try { viewDefinition.addHook(parseHook(child, viewDefinition, child)); } catch (Exception e) { throw new ViewDefinitionParserNodeException(child, e); } } } private AbstractViewHookDefinition parseHook(final Node hookNode, final ViewDefinitionImpl viewDefinition, final Node child) { HookType hookType = HookType.parseString(hookNode.getNodeName()); HookType.Category hookCategory = hookType.getCategory(); if (hookCategory == HookType.Category.CONSTRUCTION_HOOK) { return parseConstructionHook(child); } else if (hookCategory == HookType.Category.LIFECYCLE_HOOK) { return parseLifecycleHook(child); } throw new IllegalArgumentException("Unsupported hook type: " + hookType); } public ViewLifecycleHook parseLifecycleHook(final Node hookNode) { String fullyQualifiedClassName = getStringAttribute(hookNode, "class"); String methodName = getStringAttribute(hookNode, "method"); HookType hookType = HookType.parseString(hookNode.getNodeName()); return hookFactory.buildViewLifecycleHook(fullyQualifiedClassName, methodName, null, hookType); } public ViewConstructionHook parseConstructionHook(final Node hookNode) { String fullyQualifiedClassName = getStringAttribute(hookNode, "class"); String methodName = getStringAttribute(hookNode, "method"); return hookFactory.buildViewConstructionHook(fullyQualifiedClassName, methodName, null); } public ViewEventListenerHook parseEventListenerHook(final Node hookNode) { String fullyQualifiedClassName = getStringAttribute(hookNode, "class"); String methodName = getStringAttribute(hookNode, "method"); String eventName = getStringAttribute(hookNode, "event"); return hookFactory.buildViewEventListener(eventName, fullyQualifiedClassName, methodName, null); } public int getCurrentIndexOrder() { return currentIndexOrder++; } @Override public List<Node> geElementChildren(final Node node) { List<Node> result = new LinkedList<Node>(); NodeList childNodes = node.getChildNodes(); for (int i = 0; i < childNodes.getLength(); i++) { Node child = childNodes.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { result.add(child); } } return result; } @Override public ViewExtension getViewExtensionNode(final InputStream resource, final String tagType) throws ViewDefinitionParserNodeException { try { DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document document = documentBuilder.parse(resource); Node root = document.getDocumentElement(); checkState(root.getNodeName().equals(tagType), root, "Wrong root node name '" + root.getNodeName() + "'"); String plugin = getStringAttribute(root, "plugin"); String view = getStringAttribute(root, "view"); checkState(plugin != null, root, "View extension error: plugin not defined"); checkState(view != null, root, "View extension error: view not defined"); return new ViewExtension(plugin, view, root); } catch (ParserConfigurationException e) { throw new IllegalStateException(e.getMessage(), e); } catch (SAXException e) { throw new IllegalStateException(e.getMessage(), e); } catch (IOException e) { throw new IllegalStateException(e.getMessage(), e); } } @Override public InternalRibbon parseRibbon(final Node groupNode, final ViewDefinition viewDefinition) throws ViewDefinitionParserNodeException { return ribbonService.parseRibbon(groupNode, this, viewDefinition); } @Override public InternalRibbonGroup parseRibbonGroup(final Node groupNode, final ViewDefinition viewDefinition) throws ViewDefinitionParserNodeException { return ribbonService.parseRibbonGroup(groupNode, this, viewDefinition); } @Override public InternalRibbonActionItem parseRibbonItem(final Node itemNode, final ViewDefinition viewDefinition) throws ViewDefinitionParserNodeException { return ribbonService.parseRibbonItem(itemNode, this, viewDefinition); } @Override public void checkState(final boolean state, final Node node, final String message) throws ViewDefinitionParserNodeException { if (!state) { throw new ViewDefinitionParserNodeException(node, message); } } }