/** * *************************************************************************** * 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.patterns; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.qcadoo.localization.api.TranslationService; import com.qcadoo.model.api.DataDefinition; import com.qcadoo.model.api.FieldDefinition; import com.qcadoo.model.api.types.DataDefinitionHolder; import com.qcadoo.model.constants.VersionableConstants; import com.qcadoo.plugin.api.PluginUtils; import com.qcadoo.view.api.ComponentState; import com.qcadoo.view.api.ViewDefinitionState; import com.qcadoo.view.internal.ComponentDefinition; import com.qcadoo.view.internal.ComponentOption; import com.qcadoo.view.internal.FieldEntityIdChangeListener; import com.qcadoo.view.internal.ScopeEntityIdChangeListener; import com.qcadoo.view.internal.api.*; import com.qcadoo.view.internal.hooks.ViewEventListenerHook; import com.qcadoo.view.internal.states.AbstractComponentState; import com.qcadoo.view.internal.xml.ViewDefinitionParser; import com.qcadoo.view.internal.xml.ViewDefinitionParserImpl; import com.qcadoo.view.internal.xml.ViewDefinitionParserNodeException; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.springframework.context.ApplicationContext; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static org.springframework.util.StringUtils.hasText; public abstract class AbstractComponentPattern implements ComponentPattern { protected static final String JSP_PATH = "AbstractJspPath"; protected static final String JS_PATH = null; protected static final String JS_OBJECT = "AbstractJavascriptObject"; private final String name; private final String uuid; private final boolean useDto; private String extensionPluginIdentifier; private final String fieldPath; private final String scopeFieldPath; private final ComponentPattern parent; private final boolean defaultEnabled; private final boolean permanentlyDisabled; private final boolean defaultVisible; private final boolean hasDescription; private final boolean hasLabel; private final String reference; private final TranslationService translationService; private final ContextualHelpService contextualHelpService; private final ApplicationContext applicationContext; private final InternalViewDefinition viewDefinition; private final Map<String, ComponentPattern> fieldEntityIdChangeListeners = Maps.newHashMap(); private final Map<String, ComponentPattern> scopeEntityIdChangeListeners = Maps.newHashMap(); private final List<ComponentOption> options = Lists.newArrayList(); private final List<ViewEventListenerHook> customEventListeners = Lists.newArrayList(); private String script; private final List<String> scriptFiles = new ArrayList<>(); private FieldDefinition fieldDefinition; private FieldDefinition scopeFieldDefinition; private DataDefinition dataDefinition; private boolean initialized; private int indexOrder; private AbstractComponentPattern fieldComponent; private AbstractComponentPattern scopeFieldComponent; private boolean persistent; public AbstractComponentPattern(final ComponentDefinition componentDefinition) { checkArgument(hasText(componentDefinition.getName()), "Component name must be specified"); this.name = componentDefinition.getName(); this.uuid = UUID.randomUUID().toString(); this.useDto = componentDefinition.isUseDto(); this.extensionPluginIdentifier = componentDefinition.getExtensionPluginIdentifier(); this.fieldPath = componentDefinition.getFieldPath(); this.scopeFieldPath = componentDefinition.getSourceFieldPath(); this.parent = componentDefinition.getParent(); this.reference = componentDefinition.getReference(); this.hasDescription = componentDefinition.isHasDescription(); this.hasLabel = componentDefinition.isHasLabel(); this.defaultEnabled = componentDefinition.isDefaultEnabled(); this.permanentlyDisabled = componentDefinition.isPermanentlyDisabled(); this.defaultVisible = componentDefinition.isDefaultVisible(); this.translationService = componentDefinition.getTranslationService(); this.contextualHelpService = componentDefinition.getContextualHelpService(); this.dataDefinition = componentDefinition.getDataDefinition(); this.applicationContext = componentDefinition.getApplicationContext(); this.viewDefinition = (InternalViewDefinition) componentDefinition.getViewDefinition(); this.viewDefinition.registerComponent(getReference(), getPath(), this); } protected abstract String getJspFilePath(); protected abstract String getJsFilePath(); protected abstract String getJsObjectName(); protected abstract ComponentState getComponentStateInstance(); protected JSONObject getJsOptions(final Locale locale) throws JSONException { // reimplement me if you want return new JSONObject(); } protected Map<String, Object> getJspOptions(final Locale locale) { // reimplement me if you want return new HashMap<>(); } protected void initializeComponent() throws JSONException { // implement me if you want } protected void registerComponentViews(final InternalViewDefinitionService viewDefinitionService) { // implement me if you want } protected void unregisterComponentViews(final InternalViewDefinitionService viewDefinitionService) { // implement me if you want } protected ComponentPattern getParent() { return parent; } @Override public String getExtensionPluginIdentifier() { return extensionPluginIdentifier; } @Override public void registerViews(final InternalViewDefinitionService viewDefinitionService) { registerComponentViews(viewDefinitionService); } @Override public void unregisterComponent(final InternalViewDefinitionService viewDefinitionService) { viewDefinition.unregisterComponent(getReference(), getPath()); unregisterComponentViews(viewDefinitionService); if (fieldComponent != null) { if (!fieldComponent.getComponentAndField(fieldPath)[1].equals(VersionableConstants.VERSION_FIELD_NAME) || fieldDefinition != null) { fieldComponent.removeFieldEntityIdChangeListener(fieldDefinition.getName()); } } if (scopeFieldComponent != null && scopeFieldDefinition != null) { scopeFieldComponent.removeScopeEntityIdChangeListener(scopeFieldDefinition.getName()); } } @Override public InternalComponentState createComponentState(final InternalViewDefinitionState viewDefinitionState) { AbstractComponentState state = (AbstractComponentState) getComponentStateInstance(); state.setDataDefinition(dataDefinition); state.setName(name); state.setUuid(UUID.randomUUID().toString()); state.setEnabled(isDefaultEnabled()); state.setVisible(isDefaultVisible()); state.setTranslationService(translationService); state.setTranslationPath(getTranslationPath()); for (ViewEventListenerHook customEventListener : customEventListeners) { state.registerCustomEvent(customEventListener); } if (viewDefinitionState != null) { viewDefinitionState.registerComponent(getReference(), state); } return state; } @Override public Map<String, Object> prepareView(final Locale locale) { Map<String, Object> map = new HashMap<String, Object>(); map.put("name", getName()); map.put("uuid", getUuid()); map.put("path", getPath()); map.put("indexOrder", indexOrder); map.put("jspFilePath", getJspFilePath()); map.put("jsFilePath", getJsFilePath()); map.put("jsObjectName", getJsObjectName()); map.put("hasDescription", isHasDescription()); map.put("hasLabel", isHasLabel()); appendContextualHelpPath(map); Map<String, Object> jspOptions = getJspOptions(locale); jspOptions.put("defaultEnabled", isDefaultEnabled()); jspOptions.put("defaultRequired", isDefaultRequired()); jspOptions.put("defaultVisible", isDefaultVisible()); map.put("jspOptions", jspOptions); try { JSONObject jsOptions = getJsOptions(locale); addListenersToJsOptions(jsOptions); jsOptions.put("defaultEnabled", isDefaultEnabled()); jsOptions.put("defaultRequired", isDefaultRequired()); jsOptions.put("defaultVisible", isDefaultVisible()); jsOptions.put("referenceName", reference); jsOptions.put("persistent", isPersistent()); if (script != null) { jsOptions.put("script", prepareScript(script, locale)); } jsOptions.put("scriptFiles", scriptFiles); map.put("jsOptions", jsOptions); } catch (JSONException e) { throw new IllegalStateException(e.getMessage(), e); } return map; } private void appendContextualHelpPath(final Map<String, Object> map) { if (getContextualHelpService().isContextualHelpPathsVisible()) { map.put("helpPath", getContextualHelpService().getContextualHelpKey(this)); } } public String prepareScript(final String scriptBody, final Locale locale) { Pattern p = Pattern.compile("#\\{translate\\(.*?\\)\\}"); Matcher m = p.matcher(scriptBody); int lastEnd = 0; StringBuilder result = new StringBuilder(); while (m.find()) { String expression = scriptBody.substring(m.start() + 12, m.end() - 2); result.append(scriptBody.substring(lastEnd, m.start())); if (expression.contains(".")) { result.append(translationService.translate(expression, locale)); } else { result.append(translationService.translate("qcadooView.message." + expression, locale)); } lastEnd = m.end(); } if (lastEnd > 0) { result.append(scriptBody.substring(lastEnd)); return result.toString(); } else { return scriptBody; } } protected void prepareComponentView(final Map<String, Object> map, final Locale locale) throws JSONException { // implement me if you want } @Override public final String getName() { return name; } @Override public final String getUuid() { return uuid; } @Override public final String getPath() { if (parent == null) { return name; } else { return parent.getPath() + "." + name; } } @Override public String getFunctionalPath() { if (parent == null) { return name; } else { return parent.getFunctionalPath() + "." + name; } } @Override public void initializeAll() { initialize(); } @Override public boolean initialize() { if (initialized) { return true; } viewDefinition.addJsFilePath(getJsFilePath()); String[] field = null; String[] scopeField = null; if (dataDefinition == null || useDto) { if (fieldPath != null) { field = getComponentAndField(fieldPath); fieldComponent = (AbstractComponentPattern) (field[0] == null ? parent : viewDefinition .getComponentByReference(field[0])); checkNotNull(fieldComponent, "Cannot find field component for " + getPath() + ": " + fieldPath); fieldComponent.addFieldEntityIdChangeListener(field[1], this); } if (scopeFieldPath != null) { scopeField = getComponentAndField(scopeFieldPath); scopeFieldComponent = (AbstractComponentPattern) (scopeField[0] == null ? parent : viewDefinition .getComponentByReference(scopeField[0])); checkNotNull(scopeFieldComponent, "Cannot find sourceField component for " + getPath() + ": " + scopeFieldPath); scopeFieldComponent.addScopeEntityIdChangeListener(scopeField[1], this); } if (isComponentInitialized(fieldComponent) && isComponentInitialized(scopeFieldComponent)) { initialized = true; } else { return false; } } getDataDefinition(viewDefinition, fieldComponent, scopeFieldComponent, dataDefinition); getFieldAndScopeFieldDefinitions(field, fieldComponent, scopeField, scopeFieldComponent); getDataDefinitionFromFieldDefinition(); try { initializeComponent(); } catch (JSONException e) { throw new IllegalStateException(e.getMessage(), e); } return true; } public void addFieldEntityIdChangeListener(final String field, final ComponentPattern listener) { fieldEntityIdChangeListeners.put(field, listener); } public void addScopeEntityIdChangeListener(final String field, final ComponentPattern listener) { scopeEntityIdChangeListeners.put(field, listener); } public void removeFieldEntityIdChangeListener(final String field) { fieldEntityIdChangeListeners.remove(field); } public void removeScopeEntityIdChangeListener(final String field) { scopeEntityIdChangeListeners.remove(field); } public void updateComponentStateListeners(final ViewDefinitionState viewDefinitionState) { if (!fieldEntityIdChangeListeners.isEmpty()) { AbstractComponentState thisComponentState = (AbstractComponentState) viewDefinitionState .getComponentByReference(getReference()); for (Map.Entry<String, ComponentPattern> listenerPattern : fieldEntityIdChangeListeners.entrySet()) { if (isComponentEnabled(listenerPattern.getValue())) { ComponentState listenerState = viewDefinitionState.getComponentByReference(listenerPattern.getValue() .getReference()); if (listenerState != null) { thisComponentState.addFieldEntityIdChangeListener(listenerPattern.getKey(), (FieldEntityIdChangeListener) listenerState); } } } } if (!scopeEntityIdChangeListeners.isEmpty()) { AbstractComponentState thisComponentState = (AbstractComponentState) viewDefinitionState .getComponentByReference(getReference()); for (Map.Entry<String, ComponentPattern> listenerPattern : scopeEntityIdChangeListeners.entrySet()) { if (isComponentEnabled(listenerPattern.getValue())) { ComponentState listenerState = viewDefinitionState.getComponentByReference(listenerPattern.getValue() .getReference()); if (listenerState != null) { thisComponentState.addScopeEntityIdChangeListener(listenerPattern.getKey(), (ScopeEntityIdChangeListener) listenerState); } } } } } protected boolean isComponentEnabled(final ComponentPattern componentPattern) { return componentPattern.getExtensionPluginIdentifier() == null || PluginUtils.isEnabled(componentPattern.getExtensionPluginIdentifier()); } protected final Map<String, ComponentPattern> getFieldEntityIdChangeListeners() { return fieldEntityIdChangeListeners; } protected final Map<String, ComponentPattern> getScopeEntityIdChangeListeners() { return scopeEntityIdChangeListeners; } @Override public final boolean isDefaultEnabled() { return defaultEnabled; } @Override public boolean isPermanentlyDisabled() { return permanentlyDisabled; } protected final boolean isDefaultRequired() { if (getFieldDefinition() != null) { return getFieldDefinition().isRequired(); } return false; } protected final boolean isDefaultVisible() { return defaultVisible; } protected final boolean isHasDescription() { return hasDescription; } protected final boolean isHasLabel() { return hasLabel; } public final void addOption(final ComponentOption option) { options.add(option); } protected final List<ComponentOption> getOptions() { return options; } @Override public final String getReference() { if (reference == null) { return getPath(); } return reference; } @Override public final String getContextualHelpUrl() { if (getContextualHelpService() != null) { return getContextualHelpService().getHelpUrl(this); } return null; } protected final FieldDefinition getFieldDefinition() { return fieldDefinition; } protected final FieldDefinition getScopeFieldDefinition() { return scopeFieldDefinition; } public final DataDefinition getDataDefinition() { return dataDefinition; } public final TranslationService getTranslationService() { return translationService; } public ContextualHelpService getContextualHelpService() { return contextualHelpService; } public final ApplicationContext getApplicationContext() { return applicationContext; } public final ViewDefinition getViewDefinition() { return viewDefinition; } public void setExtensionPluginIdentifier(final String extensionPluginIdentifier) { this.extensionPluginIdentifier = extensionPluginIdentifier; } public final String getTranslationPath() { return getViewDefinition().getPluginIdentifier() + "." + getViewDefinition().getName() + "." + getFunctionalPath(); } private void addListenersToJsOptions(final JSONObject jsOptions) throws JSONException { JSONArray listeners = new JSONArray(); if (!fieldEntityIdChangeListeners.isEmpty() || !scopeEntityIdChangeListeners.isEmpty()) { for (ComponentPattern listener : fieldEntityIdChangeListeners.values()) { if (isComponentEnabled(listener)) { listeners.put(listener.getPath()); } } for (ComponentPattern listener : scopeEntityIdChangeListeners.values()) { if (isComponentEnabled(listener)) { listeners.put(listener.getPath()); } } } if (!customEventListeners.isEmpty()) { listeners.put(getPath()); } jsOptions.put("listeners", listeners); } private boolean isComponentInitialized(final AbstractComponentPattern fieldComponent) { return fieldComponent == null || fieldComponent.initialized; } private void getDataDefinitionFromFieldDefinition() { if (fieldDefinition != null) { getDataDefinitionFromFieldDefinition(fieldDefinition); return; } if (scopeFieldDefinition != null) { getDataDefinitionFromFieldDefinition(scopeFieldDefinition); return; } } private void getFieldAndScopeFieldDefinitions(final String[] field, final AbstractComponentPattern fieldComponent, final String[] scopeField, final AbstractComponentPattern scopeFieldComponent) { if (dataDefinition != null) { if (fieldPath != null && field[1] != null) { fieldDefinition = fieldComponent.getDataDefinition().getField(field[1]); checkNotNullFieldDefinition(fieldDefinition, field, fieldPath); } if (scopeFieldPath != null && scopeField[1] != null) { scopeFieldDefinition = scopeFieldComponent.getDataDefinition().getField(scopeField[1]); checkNotNull(scopeFieldDefinition, "Cannot find sourceField definition for " + getPath() + ": " + scopeFieldPath); } } } private void getDataDefinition(final ViewDefinition viewDefinition, final AbstractComponentPattern fieldComponent, final AbstractComponentPattern scopeFieldComponent, final DataDefinition localDataDefinition) { if (fieldPath != null && fieldComponent != null) { dataDefinition = fieldComponent.getDataDefinition(); return; } if (scopeFieldPath != null && scopeFieldComponent != null) { dataDefinition = scopeFieldComponent.getDataDefinition(); return; } if (localDataDefinition != null) { dataDefinition = localDataDefinition; return; } if (parent != null) { dataDefinition = ((AbstractComponentPattern) parent).getDataDefinition(); return; } dataDefinition = viewDefinition.getDataDefinition(); } private void getDataDefinitionFromFieldDefinition(final FieldDefinition fieldDefinition) { if (fieldDefinition.getType() instanceof DataDefinitionHolder) { dataDefinition = ((DataDefinitionHolder) fieldDefinition.getType()).getDataDefinition(); } } private String[] getComponentAndField(final String path) { Pattern pField = Pattern.compile("^#\\{(.+)\\}(\\.(\\w+))?"); Matcher mField = pField.matcher(path); if (mField.find()) { return new String[]{mField.group(1), mField.group(3)}; } else { return new String[]{null, path}; } } @Override public void addCustomEvent(final ViewEventListenerHook eventListenerHook) { customEventListeners.add(eventListenerHook); } @Override public void removeCustomEvent(final ViewEventListenerHook eventListenerHook) { customEventListeners.remove(eventListenerHook); } @Override public void parse(final Node componentNode, final ViewDefinitionParser parser) throws ViewDefinitionParserNodeException { indexOrder = ((ViewDefinitionParserImpl) parser).getCurrentIndexOrder(); persistent = getPersistentAttribute(componentNode); NodeList childNodes = componentNode.getChildNodes(); for (int i = 0; i < childNodes.getLength(); i++) { Node child = childNodes.item(i); if ("option".equals(child.getNodeName())) { addOption(parser.parseOption(child)); } else if ("listener".equals(child.getNodeName())) { addCustomEvent(parser.parseEventListener(child)); } else if ("script".equals(child.getNodeName())) { if (script == null) { script = ""; } if (nodeHasAttribute(child, "src")) { String src = child.getAttributes().getNamedItem("src").getNodeValue(); scriptFiles.add(src); } else { script += parser.getStringNodeContent(child) + ";"; } } } } @Override public boolean isPersistent() { return persistent; } private boolean getPersistentAttribute(final Node componentNode) { if (nodeHasAttribute(componentNode, "persistent")) { return Boolean.valueOf(componentNode.getAttributes().getNamedItem("persistent").toString()); } return true; } private boolean nodeHasAttribute(final Node componentNode, final String attributeName) { return componentNode.getAttributes() != null && componentNode.getAttributes().getNamedItem(attributeName) != null; } private void checkNotNullFieldDefinition(final FieldDefinition fieldDefinition, final String[] field, final String fieldPath) { if (VersionableConstants.VERSION_FIELD_NAME.equals(field[1])) { // version field, ignore if empty fieldDefinition } else { checkNotNull(fieldDefinition, "Cannot find field definition for " + getPath() + ": " + fieldPath); } } public void setPersistent(boolean persistent) { this.persistent = persistent; } public boolean isUseDto() { return useDto; } }