/* * This library is part of OpenCms - * the Open Source Content Management System * * Copyright (c) Alkacon Software GmbH (http://www.alkacon.com) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * For further information about Alkacon Software GmbH, please see the * company website: http://www.alkacon.com * * For further information about OpenCms, please see the * project website: http://www.opencms.org * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package org.opencms.workplace.editors; import org.opencms.file.CmsFile; import org.opencms.file.CmsRequestContext; import org.opencms.file.CmsResource; import org.opencms.file.CmsResourceFilter; import org.opencms.file.collectors.A_CmsResourceCollector; import org.opencms.file.collectors.I_CmsResourceCollector; import org.opencms.i18n.CmsEncoder; import org.opencms.i18n.CmsLocaleManager; import org.opencms.json.JSONArray; import org.opencms.json.JSONException; import org.opencms.json.JSONObject; import org.opencms.jsp.CmsJspActionElement; import org.opencms.lock.CmsLockType; import org.opencms.main.CmsException; import org.opencms.main.CmsLog; import org.opencms.main.OpenCms; import org.opencms.util.CmsRequestUtil; import org.opencms.util.CmsStringUtil; import org.opencms.widgets.A_CmsWidget; import org.opencms.widgets.I_CmsWidget; import org.opencms.widgets.I_CmsWidgetDialog; import org.opencms.widgets.I_CmsWidgetParameter; import org.opencms.workplace.CmsWorkplace; import org.opencms.workplace.CmsWorkplaceSettings; import org.opencms.workplace.editors.directedit.CmsDirectEditButtonSelection; import org.opencms.xml.CmsXmlContentDefinition; import org.opencms.xml.CmsXmlEntityResolver; import org.opencms.xml.CmsXmlException; import org.opencms.xml.CmsXmlUtils; import org.opencms.xml.content.CmsXmlContent; import org.opencms.xml.content.CmsXmlContentErrorHandler; import org.opencms.xml.content.CmsXmlContentFactory; import org.opencms.xml.content.CmsXmlContentTab; import org.opencms.xml.content.CmsXmlContentValueSequence; import org.opencms.xml.types.CmsXmlNestedContentDefinition; import org.opencms.xml.types.I_CmsXmlContentValue; import org.opencms.xml.types.I_CmsXmlSchemaType; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.jsp.JspException; import org.apache.commons.logging.Log; /** * Creates the editor for XML content definitions.<p> * * @since 6.0.0 */ public class CmsXmlContentEditor extends CmsEditor implements I_CmsWidgetDialog { /** Action for checking content before executing the direct edit action. */ public static final int ACTION_CHECK = 151; /** Action for confirming the XML content structure correction. */ public static final int ACTION_CONFIRMCORRECTION = 152; /** Value for the action: copy the current locale. */ public static final int ACTION_COPYLOCALE = 141; /** Action for correction of the XML content structure confirmed. */ public static final int ACTION_CORRECTIONCONFIRMED = 153; /** Action for optional element creation. */ public static final int ACTION_ELEMENT_ADD = 154; /** Action for element move down operation. */ public static final int ACTION_ELEMENT_MOVE_DOWN = 155; /** Action for element move up operation. */ public static final int ACTION_ELEMENT_MOVE_UP = 156; /** Action for optional element removal. */ public static final int ACTION_ELEMENT_REMOVE = 157; /** Action for new file creation. */ public static final int ACTION_NEW = 158; /** Action that sub choices should be determined. */ public static final int ACTION_SUBCHOICES = 159; /** Indicates that the content should be checked before executing the direct edit action. */ public static final String EDITOR_ACTION_CHECK = "check"; /** Indicates that the correction of the XML content structure should be confirmed. */ public static final String EDITOR_ACTION_CONFIRMCORRECTION = "confirmcorrect"; /** Indicates an optional element should be created. */ public static final String EDITOR_ACTION_ELEMENT_ADD = "addelement"; /** Indicates an element should be moved down. */ public static final String EDITOR_ACTION_ELEMENT_MOVE_DOWN = "elementdown"; /** Indicates an element should be moved up. */ public static final String EDITOR_ACTION_ELEMENT_MOVE_UP = "elementup"; /** Indicates an optional element should be removed. */ public static final String EDITOR_ACTION_ELEMENT_REMOVE = "removeelement"; /** Indicates a new file should be created. */ public static final String EDITOR_ACTION_NEW = CmsDirectEditButtonSelection.VALUE_NEW; /** Indicates that sub choices should be determined. */ public static final String EDITOR_ACTION_SUBCHOICES = "subchoices"; /** Indicates that the contents of the current locale should be copied to other locales. */ public static final String EDITOR_COPYLOCALE = "copylocale"; /** Indicates that the correction of the XML content structure was confirmed by the user. */ public static final String EDITOR_CORRECTIONCONFIRMED = "correctconfirmed"; /** Parameter name for the request parameter "choiceelement". */ public static final String PARAM_CHOICEELEMENT = "choiceelement"; /** Parameter name for the request parameter "choicetype". */ public static final String PARAM_CHOICETYPE = "choicetype"; /** Parameter name for the request parameter "elementindex". */ public static final String PARAM_ELEMENTINDEX = "elementindex"; /** Parameter name for the request parameter "elementname". */ public static final String PARAM_ELEMENTNAME = "elementname"; /** Parameter name for the request parameter "newlink". */ public static final String PARAM_NEWLINK = "newlink"; /** Constant for the editor type, must be the same as the editors subfolder name in the VFS. */ private static final String EDITOR_TYPE = "xmlcontent"; /** The log object for this class. */ private static final Log LOG = CmsLog.getLog(CmsXmlContentEditor.class); /** The content object to edit. */ private CmsXmlContent m_content; /** The currently active tab during form generation. */ private CmsXmlContentTab m_currentTab; /** The currently active tab index during form generation. */ private int m_currentTabIndex; /** The element locale. */ private Locale m_elementLocale; /** The list of tabs that have an element with an error. */ private List<CmsXmlContentTab> m_errorTabs; /** File object used to read and write contents. */ private CmsFile m_file; /** The set of help message IDs that have already been used. */ private Set<String> m_helpMessageIds; /** Indicates if an optional element is included in the form. */ private boolean m_optionalElementPresent; /** Parameter stores the name of the choice element to add. */ private String m_paramChoiceElement; /** Parameter stores the flag if the element to add is a choice type. */ private String m_paramChoiceType; /** Parameter stores the index of the element to add or remove. */ private String m_paramElementIndex; /** Parameter stores the name of the element to add or remove. */ private String m_paramElementName; /** The selected model file for the new resource. */ private String m_paramModelFile; /** Parameter to indicate if a new XML content resource should be created. */ private String m_paramNewLink; /** The error handler for the xml content. */ private CmsXmlContentErrorHandler m_validationHandler; /** The list of tabs that have an element with a warning. */ private List<CmsXmlContentTab> m_warningTabs; /** Visitor implementation that stored the widgets for the content. */ private CmsXmlContentWidgetVisitor m_widgetCollector; /** * Public constructor.<p> * * @param jsp an initialized JSP action element */ public CmsXmlContentEditor(CmsJspActionElement jsp) { super(jsp); } /** * Performs the change element language action of the editor.<p> */ public void actionChangeElementLanguage() { // save eventually changed content of the editor Locale oldLocale = CmsLocaleManager.getLocale(getParamOldelementlanguage()); Locale newLocale = getElementLocale(); try { setEditorValues(oldLocale); if (!m_content.validate(getCms()).hasErrors(oldLocale)) { // no errors found in content if (!m_content.hasLocale(newLocale)) { // check if we should copy the content from a default locale boolean addNew = true; List<Locale> locales = OpenCms.getLocaleManager().getDefaultLocales(getCms(), getParamResource()); if (locales.size() > 1) { // default locales have been set, try to find a match try { m_content.copyLocale(locales, newLocale); addNew = false; } catch (CmsXmlException e) { // no matching default locale was available, we will create a new one later } } if (addNew) { // create new element if selected language element is not present try { m_content.addLocale(getCms(), newLocale); } catch (CmsXmlException e) { if (LOG.isErrorEnabled()) { LOG.error(e.getLocalizedMessage(), e); } } } } //save to temporary file writeContent(); // set default action to suppress error messages setAction(ACTION_DEFAULT); } else { // errors found, switch back to old language to show errors setParamElementlanguage(getParamOldelementlanguage()); // set stored locale to null to reinitialize it m_elementLocale = null; } } catch (Exception e) { // should usually never happen if (LOG.isInfoEnabled()) { LOG.info(e.getLocalizedMessage(), e); } } } /** * Deletes the temporary file and unlocks the edited resource when in direct edit mode.<p> * * @param forceUnlock if true, the resource will be unlocked anyway */ @Override public void actionClear(boolean forceUnlock) { // delete the temporary file deleteTempFile(); boolean directEditMode = Boolean.valueOf(getParamDirectedit()).booleanValue(); boolean modified = Boolean.valueOf(getParamModified()).booleanValue(); if (directEditMode || forceUnlock || !modified) { // unlock the resource when in direct edit mode, force unlock is true or resource was not modified try { getCms().unlockResource(getParamResource()); } catch (CmsException e) { // should usually never happen if (LOG.isInfoEnabled()) { LOG.info(e.getLocalizedMessage(), e); } } } } /** * Performs the copy locale action.<p> * * @throws JspException if something goes wrong */ public void actionCopyElementLocale() throws JspException { try { setEditorValues(getElementLocale()); if (!hasValidationErrors()) { // save content of the editor only to the temporary file writeContent(); // remove eventual release & expiration date from temporary file to make preview work getCms().setDateReleased(getParamTempfile(), CmsResource.DATE_RELEASED_DEFAULT, false); getCms().setDateExpired(getParamTempfile(), CmsResource.DATE_EXPIRED_DEFAULT, false); } } catch (CmsException e) { // show error page showErrorPage(this, e); } } /** * Performs the delete locale action.<p> * * @throws JspException if something goes wrong */ public void actionDeleteElementLocale() throws JspException { try { Locale loc = getElementLocale(); m_content.removeLocale(loc); //write the modified xml content writeContent(); List<Locale> locales = m_content.getLocales(); if (locales.size() > 0) { // set first locale as new display locale Locale newLoc = locales.get(0); setParamElementlanguage(newLoc.toString()); m_elementLocale = newLoc; } else { if (LOG.isErrorEnabled()) { LOG.error(Messages.get().getBundle().key(Messages.LOG_GET_LOCALES_1, getParamResource())); } } } catch (CmsXmlException e) { // an error occurred while trying to delete the locale, stop action showErrorPage(e); } catch (CmsException e) { // should usually never happen if (LOG.isInfoEnabled()) { LOG.info(e.getLocalizedMessage(), e); } } } /** * Performs a configurable action performed by the editor.<p> * * The default action is: save resource, clear temporary files and publish the resource directly.<p> * * @throws IOException if a forward fails * @throws ServletException of a forward fails * @throws JspException if including a JSP fails */ public void actionDirectEdit() throws IOException, JspException, ServletException { // get the action class from the OpenCms runtime property I_CmsEditorActionHandler actionClass = OpenCms.getWorkplaceManager().getEditorActionHandler(); if (actionClass == null) { // error getting the action class, save content and exit the editor actionSave(); actionExit(); } else { actionClass.editorAction(this, getJsp()); } } /** * Performs the exit editor action.<p> * * @see org.opencms.workplace.editors.CmsEditor#actionExit() */ @Override public void actionExit() throws IOException, JspException, ServletException { if (getAction() == ACTION_CANCEL) { // save and exit was canceled return; } // unlock resource if we are in direct edit mode actionClear(false); // close the editor actionClose(); } /** * Moves an element in the xml content either up or down.<p> * * Depends on the given action value.<p> * * @throws JspException if including the error page fails */ public void actionMoveElement() throws JspException { // set editor values from request try { setEditorValues(getElementLocale()); } catch (CmsXmlException e) { // an error occurred while trying to set the values, stop action showErrorPage(e); return; } // get the necessary parameters to move the element int index = 0; try { index = Integer.parseInt(getParamElementIndex()); } catch (Exception e) { // ignore, should not happen } // get the value to move I_CmsXmlContentValue value = m_content.getValue(getParamElementName(), getElementLocale(), index); if (getAction() == ACTION_ELEMENT_MOVE_DOWN) { // move down the value value.moveDown(); } else { // move up the value value.moveUp(); } if (getValidationHandler().hasWarnings(getElementLocale())) { // there were warnings for the edited content, reset validation handler to avoid display issues resetErrorHandler(); } try { // write the modified content to the temporary file writeContent(); } catch (CmsException e) { // an error occurred while trying to save showErrorPage(e); } } /** * Creates a new XML content item for editing.<p> * * @throws JspException in case something goes wrong */ public void actionNew() throws JspException { // get the collector used to create the new content int pos = m_paramNewLink.indexOf('|'); String collectorName = m_paramNewLink.substring(0, pos); String collectorParams = m_paramNewLink.substring(pos + 1); String param; String templateFileName; pos = collectorParams.indexOf(A_CmsResourceCollector.SEPARATOR_TEMPLATEFILE); if (pos != -1) { // found an explicit template file name to use for the new resource, use it param = collectorParams.substring(0, pos); templateFileName = collectorParams.substring(pos + A_CmsResourceCollector.SEPARATOR_TEMPLATEFILE.length()); } else { // no template file name was specified, use given resource name as template file param = collectorParams; templateFileName = getParamResource(); } // get the collector used for calculating the next file name I_CmsResourceCollector collector = OpenCms.getResourceManager().getContentCollector(collectorName); String newFileName = ""; try { // one resource serves as a "template" for the new resource CmsFile templateFile = getCms().readFile(templateFileName, CmsResourceFilter.IGNORE_EXPIRATION); CmsXmlContent template = CmsXmlContentFactory.unmarshal(getCloneCms(), templateFile); // set the required content locale Locale locale = getElementLocale(); // now create a new XML content based on the templates content definition CmsXmlContent newContent = CmsXmlContentFactory.createDocument( getCms(), locale, template.getEncoding(), template.getContentDefinition()); // IMPORTANT: calculation of the name MUST be done here so the file name is ensured to be valid newFileName = collector.getCreateLink(getCms(), collectorName, param); boolean useModelFile = false; if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(getParamModelFile())) { getCms().getRequestContext().setAttribute(CmsRequestContext.ATTRIBUTE_MODEL, getParamModelFile()); useModelFile = true; } // now create the resource, fill it with the marshalled XML and write it back to the VFS getCms().createResource(newFileName, templateFile.getTypeId()); // re-read the created resource CmsFile newFile = getCms().readFile(newFileName, CmsResourceFilter.ALL); if (!useModelFile) { newFile.setContents(newContent.marshal()); // write the file with the updated content getCloneCms().writeFile(newFile); } // wipe out parameters for the editor to ensure proper operation setParamNewLink(null); setParamAction(null); setParamResource(newFileName); setAction(ACTION_DEFAULT); // create the temporary file to work with setParamTempfile(createTempFile()); // set the member variables for the content m_file = getCms().readFile(getParamTempfile(), CmsResourceFilter.ALL); if (!useModelFile) { m_content = newContent; } else { m_content = CmsXmlContentFactory.unmarshal(getCms(), m_file); } } catch (CmsException e) { if (LOG.isErrorEnabled()) { LOG.error(Messages.get().getBundle().key(Messages.LOG_CREATE_XML_CONTENT_ITEM_1, m_paramNewLink), e); } throw new JspException(e); } finally { try { // delete the new file getCms().deleteResource(newFileName, CmsResource.DELETE_REMOVE_SIBLINGS); } catch (CmsException e) { // ignore } } } /** * Performs the preview XML content action in a new browser window.<p> * * @throws IOException if redirect fails * @throws JspException if inclusion of error page fails */ public void actionPreview() throws IOException, JspException { try { // save content of the editor only to the temporary file setEditorValues(getElementLocale()); writeContent(); // remove eventual release & expiration date from temporary file to make preview work getCms().setDateReleased(getParamTempfile(), CmsResource.DATE_RELEASED_DEFAULT, false); getCms().setDateExpired(getParamTempfile(), CmsResource.DATE_EXPIRED_DEFAULT, false); } catch (CmsException e) { // show error page showErrorPage(this, e); } // get preview uri from content handler String previewUri = m_content.getHandler().getPreview(getCms(), m_content, getParamTempfile()); // create locale request parameter StringBuffer param = new StringBuffer(8); if (previewUri.indexOf('?') != -1) { param.append("&"); } else { param.append("?"); } param.append(CmsLocaleManager.PARAMETER_LOCALE); param.append("="); param.append(getParamElementlanguage()); // redirect to the temporary file with currently active element language or to the specified preview uri sendCmsRedirect(previewUri + param); } /** * Performs the save content action.<p> * * @see org.opencms.workplace.editors.CmsEditor#actionSave() */ @Override public void actionSave() throws JspException { actionSave(getElementLocale()); if (getAction() != ACTION_CANCEL) { // save successful, set save action setAction(ACTION_SAVE); } } /** * Performs the save content action.<p> * * This is also used when changing the element language.<p> * * @param locale the locale to save the content * @throws JspException if including the error page fails */ public void actionSave(Locale locale) throws JspException { try { setEditorValues(locale); // check if content has errors if (!hasValidationErrors()) { // no errors found, write content and copy temp file contents writeContent(); commitTempFile(); // set the modified parameter setParamModified(Boolean.TRUE.toString()); } } catch (CmsException e) { showErrorPage(e); } } /** * Adds an optional element to the XML content or removes an optional element from the XML content.<p> * * Depends on the given action value.<p> * * @throws JspException if including the error page fails */ public void actionToggleElement() throws JspException { // set editor values from request try { setEditorValues(getElementLocale()); } catch (CmsXmlException e) { // an error occurred while trying to set the values, stop action showErrorPage(e); return; } // get the necessary parameters to add/remove the element int index = 0; try { index = Integer.parseInt(getParamElementIndex()); } catch (Exception e) { // ignore, should not happen } if (getAction() == ACTION_ELEMENT_REMOVE) { // remove the value , first get the value to remove I_CmsXmlContentValue value = m_content.getValue(getParamElementName(), getElementLocale(), index); m_content.removeValue(getParamElementName(), getElementLocale(), index); // check if the value was a choice option and the last one if (value.isChoiceOption() && (m_content.getSubValues( CmsXmlUtils.removeLastXpathElement(getParamElementName()), getElementLocale()).size() == 0)) { // also remove the parent choice type value String xpath = CmsXmlUtils.removeLastXpathElement(getParamElementName()); m_content.removeValue(xpath, getElementLocale(), CmsXmlUtils.getXpathIndexInt(xpath) - 1); } } else { // add the new value after the clicked element if (m_content.hasValue(getParamElementName(), getElementLocale())) { // when other values are present, increase index to use right position index += 1; } String elementPath = getParamElementName(); if (CmsStringUtil.isNotEmpty(getParamChoiceElement())) { // we have to add a choice element, first check if the element to add itself is part of a choice or not boolean choiceType = Boolean.valueOf(getParamChoiceType()).booleanValue(); I_CmsXmlSchemaType elemType = m_content.getContentDefinition().getSchemaType(elementPath); if (!choiceType || (elemType.isChoiceOption() && elemType.isChoiceType())) { // this is a choice option or a nested choice type to add, remove last element name from xpath elementPath = CmsXmlUtils.removeLastXpathElement(elementPath); } else { // this is a choice type to add, first create type element m_content.addValue(getCms(), elementPath, getElementLocale(), index); elementPath = CmsXmlUtils.createXpathElement(elementPath, index + 1); // all eventual following elements to create have to be at first position index = 0; } // check if there are nested choice elements to add if (CmsXmlUtils.isDeepXpath(getParamChoiceElement())) { // create all missing elements except the last one String pathToChoice = CmsXmlUtils.removeLastXpathElement(getParamChoiceElement()); String newPath = elementPath; while (CmsStringUtil.isNotEmpty(pathToChoice)) { String createElement = CmsXmlUtils.getFirstXpathElement(pathToChoice); newPath = CmsXmlUtils.concatXpath(newPath, createElement); pathToChoice = CmsXmlUtils.isDeepXpath(pathToChoice) ? CmsXmlUtils.removeFirstXpathElement(pathToChoice) : null; I_CmsXmlContentValue newVal = m_content.addValue(getCms(), newPath, getElementLocale(), index); newPath = newVal.getPath(); // all eventual following elements to create have to be at first position index = 0; } // create the path to the last choice element elementPath = CmsXmlUtils.concatXpath( newPath, CmsXmlUtils.getLastXpathElement(getParamChoiceElement())); } else { // create the path to the choice element elementPath += "/" + getParamChoiceElement(); } } // add the value m_content.addValue(getCms(), elementPath, getElementLocale(), index); } if (getValidationHandler().hasWarnings(getElementLocale())) { // there were warnings for the edited content, reset validation handler to avoid display issues resetErrorHandler(); } try { // write the modified content to the temporary file writeContent(); } catch (CmsException e) { // an error occurred while trying to save showErrorPage(e); } } /** * Returns the JSON array with information about the choices of a given element.<p> * * The returned array is only filled if the given element has choice options, otherwise an empty array is returned.<br/> * Note: the first array element is an object containing information if the element itself is a choice type, * the following elements are the choice option items.<p> * * @param elementName the element name to check (complete xpath) * @param choiceType flag indicating if the given element name represents a choice type or not * @param checkChoice flag indicating if the element name should be checked if it is a choice option and choice type * * @return the JSON array with information about the choices of a given element */ public JSONArray buildElementChoices(String elementName, boolean choiceType, boolean checkChoice) { JSONArray choiceElements = new JSONArray(); String choiceName = elementName; I_CmsXmlSchemaType elemType = m_content.getContentDefinition().getSchemaType(elementName); if (checkChoice && elemType.isChoiceOption() && elemType.isChoiceType()) { // the element itself is a choice option and again a choice type, remove the last element to get correct choices choiceName = CmsXmlUtils.removeLastXpathElement(elementName); } // use xpath to get choice information if (m_content.hasChoiceOptions(choiceName, getElementLocale())) { // we have choice options, first add information about type to create JSONObject info = new JSONObject(); try { // put information if element is a choice type info.put("choicetype", choiceType); choiceElements.put(info); // get the available choice options for the choice element List<I_CmsXmlSchemaType> options = m_content.getChoiceOptions(choiceName, getElementLocale()); for (Iterator<I_CmsXmlSchemaType> i = options.iterator(); i.hasNext();) { // add the available element options I_CmsXmlSchemaType type = i.next(); JSONObject option = new JSONObject(); String key = A_CmsWidget.LABEL_PREFIX + type.getContentDefinition().getInnerName() + "." + type.getName(); // add element name, label and help info option.put("name", type.getName()); option.put("label", keyDefault(key, type.getName())); option.put("help", keyDefault(key + A_CmsWidget.HELP_POSTFIX, "")); // add info if the choice itself is a (sub) choice type option.put("subchoice", type.isChoiceType()); choiceElements.put(option); } } catch (JSONException e) { // ignore, should not happen } } return choiceElements; } /** * Builds the HTML String for the element language selector.<p> * * This method has to use the resource request parameter because the temporary file is * not available in the upper button frame.<p> * * @param attributes optional attributes for the <select> tag * @return the HTML for the element language select box */ public String buildSelectElementLanguage(String attributes) { return buildSelectElementLanguage(attributes, getParamResource(), getElementLocale()); } /** * Returns the available sub choices for a nested choice element.<p> * * @return the available sub choices for a nested choice element as JSON array string */ public String buildSubChoices() { String elementPath = getParamElementName(); // we have to add a choice element, first check if the element to add itself is part of a choice or not boolean choiceType = Boolean.valueOf(getParamChoiceType()).booleanValue(); I_CmsXmlSchemaType elemType = m_content.getContentDefinition().getSchemaType(elementPath); if (!choiceType || (elemType.isChoiceOption() && elemType.isChoiceType())) { // this is a choice option or a nested choice type to add, remove last element name from xpath elementPath = CmsXmlUtils.removeLastXpathElement(elementPath); } elementPath = CmsXmlUtils.concatXpath(elementPath, getParamChoiceElement()); return buildElementChoices(elementPath, choiceType, false).toString(); } /** * @see org.opencms.widgets.I_CmsWidgetDialog#getButtonStyle() */ public int getButtonStyle() { return getSettings().getUserSettings().getEditorButtonStyle(); } /** * @see org.opencms.workplace.editors.CmsEditor#getEditorResourceUri() */ @Override public String getEditorResourceUri() { return getSkinUri() + "editors/" + EDITOR_TYPE + "/"; } /** * Returns the current element locale.<p> * * @return the current element locale */ public Locale getElementLocale() { if (m_elementLocale == null) { if (CmsStringUtil.isNotEmpty(getParamElementlanguage()) && !"null".equals(getParamElementlanguage())) { m_elementLocale = CmsLocaleManager.getLocale(getParamElementlanguage()); } else { initElementLanguage(); m_elementLocale = CmsLocaleManager.getLocale(getParamElementlanguage()); } } return m_elementLocale; } /** * @see org.opencms.widgets.I_CmsWidgetDialog#getHelpMessageIds() */ public Set<String> getHelpMessageIds() { if (m_helpMessageIds == null) { m_helpMessageIds = new HashSet<String>(); } return m_helpMessageIds; } /** * Returns the name of the choice element to add.<p> * * @return the name of the choice element to add */ public String getParamChoiceElement() { return m_paramChoiceElement; } /** * Returns the flag if the element to add is a choice type.<p> * * @return the flag if the element to add is a choice type */ public String getParamChoiceType() { return m_paramChoiceType; } /** * Returns the index of the element to add or remove.<p> * * @return the index of the element to add or remove */ public String getParamElementIndex() { return m_paramElementIndex; } /** * Returns the name of the element to add or remove.<p> * * @return the name of the element to add or remove */ public String getParamElementName() { return m_paramElementName; } /** * Returns the parameter that specifies the model file name.<p> * * @return the parameter that specifies the model file name */ public String getParamModelFile() { return m_paramModelFile; } /** * Returns the "new link" parameter.<p> * * @return the "new link" parameter */ public String getParamNewLink() { return m_paramNewLink; } /** * @see org.opencms.widgets.I_CmsWidgetDialog#getUserAgent() */ public String getUserAgent() { return getJsp().getRequest().getHeader(CmsRequestUtil.HEADER_USER_AGENT); } /** * Returns the different xml editor widgets used in the form to display.<p> * * @return the different xml editor widgets used in the form to display */ public CmsXmlContentWidgetVisitor getWidgetCollector() { if (m_widgetCollector == null) { // create an instance of the widget collector m_widgetCollector = new CmsXmlContentWidgetVisitor(getElementLocale()); m_content.visitAllValuesWith(m_widgetCollector); } return m_widgetCollector; } /** * Generates the HTML form for the XML content editor.<p> * * @return the HTML that generates the form for the XML editor */ public String getXmlEditorForm() { // set "editor mode" attribute (required for link replacement in the root site) getCms().getRequestContext().setAttribute(CmsRequestContext.ATTRIBUTE_EDITOR, Boolean.TRUE); // add customized message bundle eventually specified in XSD of XML content addMessages(m_content.getHandler().getMessages(getLocale())); // initialize tab lists for error handling before generating the editor form m_errorTabs = new ArrayList<CmsXmlContentTab>(); m_warningTabs = new ArrayList<CmsXmlContentTab>(); return getXmlEditorForm(m_content.getContentDefinition(), "", true, false).toString(); } /** * Generates the HTML for the end of the HTML editor form page.<p> * * @return the HTML for the end of the HTML editor form page * @throws JspException if including the error page fails */ public String getXmlEditorHtmlEnd() throws JspException { StringBuffer result = new StringBuffer(16384); if (m_optionalElementPresent) { // disabled optional element(s) present, reset widgets to show help bubbles on optional form entries resetWidgetCollector(); } try { // get all widgets from collector Iterator<String> i = getWidgetCollector().getWidgets().keySet().iterator(); while (i.hasNext()) { // get the value of the widget String key = i.next(); I_CmsXmlContentValue value = getWidgetCollector().getValues().get(key); I_CmsWidget widget = getWidgetCollector().getWidgets().get(key); result.append(widget.getDialogHtmlEnd(getCms(), this, (I_CmsWidgetParameter)value)); } // add empty help text layer result.append("<div class=\"help\" id=\"helpText\" "); result.append("onmouseover=\"showHelpText();\" onmouseout=\"hideHelpText();\"></div>\n"); // add empty element button layer result.append("<div class=\"xmlButtons\" id=\"xmlElementButtons\" "); result.append("onmouseover=\"checkElementButtons(true);\" onmouseout=\"checkElementButtons(false);\"></div>\n"); // return the HTML return result.toString(); } catch (Exception e) { showErrorPage(e); return ""; } } /** * Generates the JavaScript includes for the used widgets in the editor form.<p> * * @return the JavaScript includes for the used widgets * @throws JspException if including the error page fails */ public String getXmlEditorIncludes() throws JspException { StringBuffer result = new StringBuffer(1024); // first include general JQuery JS and UI components result.append("<script type=\"text/javascript\" src=\""); result.append(CmsWorkplace.getSkinUri()).append("jquery/packed/jquery.js"); result.append("\"></script>\n"); result.append("<script type=\"text/javascript\" src=\""); result.append(CmsWorkplace.getSkinUri()).append("jquery/packed/jquery.ui.js"); result.append("\"></script>\n"); // including dialog-helper.js to be used by ADE gallery widgets result.append("<script type=\"text/javascript\" src=\""); result.append(CmsWorkplace.getSkinUri()).append("components/widgets/dialog-helper.js"); result.append("\"></script>\n"); // import the JavaScript for JSON helper functions result.append("<script type=\"text/javascript\" src=\""); result.append(CmsWorkplace.getSkinUri()).append("commons/json2.js"); result.append("\"></script>\n"); result.append("<link rel=\"stylesheet\" type=\"text/css\" href=\""); result.append(CmsWorkplace.getSkinUri()).append("jquery/css/ui-ocms/jquery.ui.css"); result.append("\">\n"); result.append("<link rel=\"stylesheet\" type=\"text/css\" href=\""); result.append(CmsWorkplace.getSkinUri()).append("jquery/css/ui-ocms/jquery.ui.ocms.css"); result.append("\">\n"); try { // iterate over unique widgets from collector Iterator<I_CmsWidget> i = getWidgetCollector().getUniqueWidgets().iterator(); while (i.hasNext()) { I_CmsWidget widget = i.next(); result.append(widget.getDialogIncludes(getCms(), this)); result.append("\n"); } } catch (Exception e) { showErrorPage(e); } return result.toString(); } /** * Generates the JavaScript initialization calls for the used widgets in the editor form.<p> * * @return the JavaScript initialization calls for the used widgets * @throws JspException if including the error page fails */ public String getXmlEditorInitCalls() throws JspException { StringBuffer result = new StringBuffer(512); try { // iterate over unique widgets from collector Iterator<I_CmsWidget> i = getWidgetCollector().getUniqueWidgets().iterator(); while (i.hasNext()) { I_CmsWidget widget = i.next(); result.append(widget.getDialogInitCall(getCms(), this)); } } catch (Exception e) { showErrorPage(e); } return result.toString(); } /** * Generates the JavaScript initialization methods for the used widgets.<p> * * @return the JavaScript initialization methods for the used widgets * * @throws JspException if an error occurs during JavaScript generation */ public String getXmlEditorInitMethods() throws JspException { StringBuffer result = new StringBuffer(1024); // first create JS for eventual tabs StringBuffer tabs = new StringBuffer(512); if (m_content.getHandler().getTabs().size() > 0) { // we have some tabs defined, initialize them using JQuery result.append("var xmlSelectedTab = 0;\n"); result.append("var xmlErrorTabs = new Array();\n"); result.append("var xmlWarningTabs = new Array();\n"); tabs.append("\t$xmltabs = $(\"#xmltabs\").tabs({});\n"); tabs.append("\t$xmltabs.tabs(\"select\", xmlSelectedTab);\n"); tabs.append("\tfor (var i=0; i<xmlErrorTabs.length; i++) {\n"); tabs.append("\t\t$(\"#OcmsTabTab\" + xmlErrorTabs[i]).addClass(\"ui-state-error\");\n"); tabs.append("\t}\n"); tabs.append("\tfor (var i=0; i<xmlWarningTabs.length; i++) {\n"); tabs.append("\t\t$(\"#OcmsTabTab\" + xmlWarningTabs[i]).addClass(\"ui-state-warning\");\n"); tabs.append("\t}\n"); } // create JS for UI dialog result.append("var dialogTitleAddChoice = \""); result.append(key(Messages.GUI_EDITOR_XMLCONTENT_CHOICE_ADD_HL_0)); result.append("\";\n"); result.append("var dialogTitleAddSubChoice = \""); result.append(key(Messages.GUI_EDITOR_XMLCONTENT_CHOICE_SUB_ADD_HL_0)); result.append("\";\n"); result.append("var vfsPathEditorForm = \""); result.append(getJsp().link(CmsEditor.PATH_EDITORS + "xmlcontent/editor_form.jsp")); result.append("\";\n"); result.append("if (jQuery) {\n"); result.append("$(document).ready(function(){\n"); result.append(tabs); result.append("\t$(\"#xmladdelementdialog\").dialog({\n"); result.append("\t\ttitle: \""); result.append(key(Messages.GUI_EDITOR_XMLCONTENT_CHOICE_ADD_HL_0)); result.append("\",\n"); result.append("\t\tautoOpen: false,\n"); result.append("\t\tbgiframe: true,\n"); result.append("\t\tminHeight: 150,\n"); result.append("\t\tminWidth: 300,\n"); result.append("\t\twidth: 360,\n"); result.append("\t\tmodal: true,\n"); result.append("\t\tbuttons: { \""); result.append(key(org.opencms.workplace.Messages.GUI_DIALOG_BUTTON_CANCEL_0)); result.append("\": function() { $(this).dialog(\"close\"); } }\n"); result.append("\t});\n"); result.append("});\n"); result.append("}\n"); try { // iterate over unique widgets from collector Iterator<I_CmsWidget> i = getWidgetCollector().getUniqueWidgets().iterator(); while (i.hasNext()) { I_CmsWidget widget = i.next(); result.append(widget.getDialogInitMethod(getCms(), this)); result.append("\n"); } } catch (Exception e) { showErrorPage(e); } return result.toString(); } /** * Returns true if the edited content contains validation errors, otherwise false.<p> * * @return true if the edited content contains validation errors, otherwise false */ public boolean hasValidationErrors() { return getValidationHandler().hasErrors(); } /** * Returns true if the preview is available for the edited xml content.<p> * * This method has to use the resource request parameter and read the file from vfs because the temporary file is * not available in the upper button frame.<p> * * @return true if the preview is enabled, otherwise false */ public boolean isPreviewEnabled() { try { // read the original file because temporary file is not created when opening button frame CmsFile file = getCms().readFile(getParamResource(), CmsResourceFilter.ALL); CmsXmlContent content = CmsXmlContentFactory.unmarshal(getCloneCms(), file); return content.getHandler().getPreview(getCms(), m_content, getParamResource()) != null; } catch (Exception e) { // error reading or unmarshalling, no preview available return false; } } /** * Sets the editor values for the locale with the parameters from the request.<p> * * Called before saving the xml content, redisplaying the input form, * changing the language and adding or removing elements.<p> * * @param locale the locale of the content to save * @throws CmsXmlException if something goes wrong */ public void setEditorValues(Locale locale) throws CmsXmlException { List<String> names = m_content.getNames(locale); Iterator<String> i = names.iterator(); while (i.hasNext()) { String path = i.next(); I_CmsXmlContentValue value = m_content.getValue(path, locale); if (value.isSimpleType()) { I_CmsWidget widget = value.getContentDefinition().getContentHandler().getWidget(value); widget.setEditorValue( getCms(), getJsp().getRequest().getParameterMap(), this, (I_CmsWidgetParameter)value); } } } /** * Sets the name of the choice element to add.<p> * * @param choiceElement the name of the choice element to add */ public void setParamChoiceElement(String choiceElement) { m_paramChoiceElement = choiceElement; } /** * Sets the flag if the element to add is a choice type.<p> * * @param paramChoiceType the flag if the element to add is a choice type */ public void setParamChoiceType(String paramChoiceType) { m_paramChoiceType = paramChoiceType; } /** * Sets the index of the element to add or remove.<p> * * @param elementIndex the index of the element to add or remove */ public void setParamElementIndex(String elementIndex) { m_paramElementIndex = elementIndex; } /** * Sets the name of the element to add or remove.<p> * * @param elementName the name of the element to add or remove */ public void setParamElementName(String elementName) { m_paramElementName = elementName; } /** * Sets the parameter that specifies the model file name.<p> * * @param paramMasterFile the parameter that specifies the model file name */ public void setParamModelFile(String paramMasterFile) { m_paramModelFile = paramMasterFile; } /** * Sets the "new link" parameter.<p> * * @param paramNewLink the "new link" parameter to set */ public void setParamNewLink(String paramNewLink) { m_paramNewLink = CmsEncoder.decode(paramNewLink); } /** * Determines if the element language selector is shown dependent on the available Locales.<p> * * @return true, if more than one Locale is available, otherwise false */ public boolean showElementLanguageSelector() { List<Locale> locales = OpenCms.getLocaleManager().getAvailableLocales(getCms(), getParamResource()); if ((locales == null) || (locales.size() < 2)) { // for less than two available locales, do not create language selector return false; } return true; } /** * @see org.opencms.workplace.tools.CmsToolDialog#useNewStyle() */ @Override public boolean useNewStyle() { return false; } /** * @see org.opencms.workplace.editors.CmsEditor#commitTempFile() */ @Override protected void commitTempFile() throws CmsException { super.commitTempFile(); m_file = getCloneCms().readFile(getParamResource()); m_content = CmsXmlContentFactory.unmarshal(getCloneCms(), m_file); } /** * Makes sure the requested locale node is present in the content document * by either copying an existing locale node or creating an empty one.<p> * * @param locale the requested locale * * @return the locale */ protected Locale ensureLocale(Locale locale) { // get the default locale for the resource List<Locale> locales = OpenCms.getLocaleManager().getDefaultLocales(getCms(), getParamResource()); if (m_content != null) { if (!m_content.hasLocale(locale)) { try { // to copy anything we need at least one locale if ((m_content.getLocales().size() > 0)) { // required locale not available, check if an existing default locale should be copied as "template" try { // a list of possible default locales has been set as property, try to find a match m_content.copyLocale(locales, locale); } catch (CmsException e) { m_content.addLocale(getCms(), locale); } } else { m_content.addLocale(getCms(), locale); } writeContent(); } catch (CmsException e) { LOG.error(e.getMessageContainer(), e); } } if (!m_content.hasLocale(locale)) { // value may have changed because of the copy operation locale = m_content.getLocales().get(0); } } return locale; } /** * Initializes the editor content when opening the editor for the first time.<p> * * Not necessary for the xmlcontent editor.<p> */ @Override protected void initContent() { // nothing to be done for the xmlcontent editor form } /** * Initializes the element language for the first call of the editor.<p> */ protected void initElementLanguage() { // get the default locale for the resource List<Locale> locales = OpenCms.getLocaleManager().getDefaultLocales(getCms(), getParamResource()); Locale locale = locales.get(0); locale = ensureLocale(locale); setParamElementlanguage(locale.toString()); } /** * @see org.opencms.workplace.CmsWorkplace#initWorkplaceRequestValues(org.opencms.workplace.CmsWorkplaceSettings, javax.servlet.http.HttpServletRequest) */ @Override protected void initWorkplaceRequestValues(CmsWorkplaceSettings settings, HttpServletRequest request) { // fill the parameter values in the get/set methods fillParamValues(request); // set the dialog type setParamDialogtype(EDITOR_TYPE); if (getParamNewLink() != null) { setParamAction(EDITOR_ACTION_NEW); } else { // initialize a content object from the temporary file if ((getParamTempfile() != null) && !"null".equals(getParamTempfile())) { try { m_file = getCms().readFile(this.getParamTempfile(), CmsResourceFilter.ALL); m_content = CmsXmlContentFactory.unmarshal(getCloneCms(), m_file); } catch (CmsException e) { // error during initialization, show error page try { showErrorPage(this, e); } catch (JspException exc) { // should usually never happen if (LOG.isInfoEnabled()) { LOG.info(exc); } } } } } // set the action for the JSP switch if (EDITOR_SAVE.equals(getParamAction())) { setAction(ACTION_SAVE); } else if (EDITOR_SAVEEXIT.equals(getParamAction())) { setAction(ACTION_SAVEEXIT); } else if (EDITOR_EXIT.equals(getParamAction())) { setAction(ACTION_EXIT); } else if (EDITOR_ACTION_SUBCHOICES.equals(getParamAction())) { setAction(ACTION_SUBCHOICES); } else if (EDITOR_CLOSEBROWSER.equals(getParamAction())) { // closed browser window accidentally, unlock resource and delete temporary file actionClear(true); return; } else if (EDITOR_ACTION_CHECK.equals(getParamAction())) { setAction(ACTION_CHECK); } else if (EDITOR_SAVEACTION.equals(getParamAction())) { setAction(ACTION_SAVEACTION); try { actionDirectEdit(); } catch (Exception e) { // should usually never happen if (LOG.isInfoEnabled()) { LOG.info(e.getLocalizedMessage(), e); } } setAction(ACTION_EXIT); } else if (EDITOR_COPYLOCALE.equals(getParamAction())) { setAction(ACTION_COPYLOCALE); } else if (EDITOR_DELETELOCALE.equals(getParamAction())) { setAction(ACTION_DELETELOCALE); } else if (EDITOR_SHOW.equals(getParamAction())) { setAction(ACTION_SHOW); } else if (EDITOR_SHOW_ERRORMESSAGE.equals(getParamAction())) { setAction(ACTION_SHOW_ERRORMESSAGE); } else if (EDITOR_CHANGE_ELEMENT.equals(getParamAction())) { setAction(ACTION_SHOW); actionChangeElementLanguage(); } else if (EDITOR_ACTION_ELEMENT_ADD.equals(getParamAction())) { setAction(ACTION_ELEMENT_ADD); try { actionToggleElement(); } catch (JspException e) { if (LOG.isErrorEnabled()) { LOG.error(org.opencms.workplace.Messages.get().getBundle().key( org.opencms.workplace.Messages.LOG_INCLUDE_ERRORPAGE_FAILED_0)); } } } else if (EDITOR_ACTION_ELEMENT_REMOVE.equals(getParamAction())) { setAction(ACTION_ELEMENT_REMOVE); try { actionToggleElement(); } catch (JspException e) { if (LOG.isErrorEnabled()) { LOG.error(org.opencms.workplace.Messages.get().getBundle().key( org.opencms.workplace.Messages.LOG_INCLUDE_ERRORPAGE_FAILED_0)); } } } else if (EDITOR_ACTION_ELEMENT_MOVE_DOWN.equals(getParamAction())) { setAction(ACTION_ELEMENT_MOVE_DOWN); try { actionMoveElement(); } catch (JspException e) { if (LOG.isErrorEnabled()) { LOG.error(org.opencms.workplace.Messages.get().getBundle().key( org.opencms.workplace.Messages.LOG_INCLUDE_ERRORPAGE_FAILED_0)); } } } else if (EDITOR_ACTION_ELEMENT_MOVE_UP.equals(getParamAction())) { setAction(ACTION_ELEMENT_MOVE_UP); try { actionMoveElement(); } catch (JspException e) { if (LOG.isErrorEnabled()) { LOG.error(org.opencms.workplace.Messages.get().getBundle().key( org.opencms.workplace.Messages.LOG_INCLUDE_ERRORPAGE_FAILED_0)); } } } else if (EDITOR_ACTION_NEW.equals(getParamAction())) { setAction(ACTION_NEW); return; } else if (EDITOR_PREVIEW.equals(getParamAction())) { setAction(ACTION_PREVIEW); } else if (EDITOR_CORRECTIONCONFIRMED.equals(getParamAction())) { setAction(ACTION_SHOW); try { // correct the XML structure before showing the form correctXmlStructure(); } catch (CmsException e) { // error during correction try { showErrorPage(this, e); } catch (JspException exc) { // should usually never happen if (LOG.isInfoEnabled()) { LOG.info(exc); } } } } else { // initial call of editor setAction(ACTION_DEFAULT); try { // lock resource if autolock is enabled in configuration if (Boolean.valueOf(getParamDirectedit()).booleanValue()) { // set a temporary lock in direct edit mode checkLock(getParamResource(), CmsLockType.TEMPORARY); } else { // set common lock checkLock(getParamResource()); } // create the temporary file setParamTempfile(createTempFile()); // initialize a content object from the created temporary file m_file = getCms().readFile(this.getParamTempfile(), CmsResourceFilter.ALL); m_content = CmsXmlContentFactory.unmarshal(getCloneCms(), m_file); // check the XML content against the given XSD try { m_content.validateXmlStructure(new CmsXmlEntityResolver(getCms())); } catch (CmsXmlException eXml) { // validation failed, check the settings for handling the correction if (OpenCms.getWorkplaceManager().isXmlContentAutoCorrect()) { // correct the XML structure automatically according to the XSD correctXmlStructure(); } else { // show correction confirmation dialog setAction(ACTION_CONFIRMCORRECTION); } } } catch (CmsException e) { // error during initialization try { showErrorPage(this, e); } catch (JspException exc) { // should usually never happen if (LOG.isInfoEnabled()) { LOG.info(exc); } } } // set the initial element language if not given in request parameters if (getParamElementlanguage() == null) { initElementLanguage(); } else { Locale locale = CmsLocaleManager.getLocale(getParamElementlanguage()); ensureLocale(locale); } } } /** * Returns the HTML for the element operation buttons add, move, remove.<p> * * @param value the value for which the buttons are generated * @param index the index of the element * @param addElement if true, the button to add an element is shown * @param removeElement if true, the button to remove an element is shown * @return the HTML for the element operation buttons */ private String buildElementButtons(I_CmsXmlContentValue value, boolean addElement, boolean removeElement) { StringBuffer jsCall = new StringBuffer(512); String elementName = CmsXmlUtils.removeXpathIndex(value.getPath()); // indicates if at least one button is active boolean buttonPresent = false; int index = value.getIndex(); jsCall.append("showElementButtons('"); jsCall.append(elementName); jsCall.append("', "); jsCall.append(index); jsCall.append(", "); // build the remove element button if required if (removeElement) { jsCall.append(Boolean.TRUE); buttonPresent = true; } else { jsCall.append(Boolean.FALSE); } jsCall.append(", "); // build the move down button (move down in API is move up for content editor) if (index > 0) { // build active move down button jsCall.append(Boolean.TRUE); buttonPresent = true; } else { jsCall.append(Boolean.FALSE); } jsCall.append(", "); // build the move up button (move up in API is move down for content editor) int indexCount = m_content.getIndexCount(elementName, getElementLocale()); if (index < (indexCount - 1)) { // build active move up button jsCall.append(Boolean.TRUE); buttonPresent = true; } else { jsCall.append(Boolean.FALSE); } jsCall.append(", "); // build the add element button if required if (addElement) { jsCall.append(Boolean.TRUE); buttonPresent = true; } else { jsCall.append(Boolean.FALSE); } jsCall.append(", "); JSONArray newElements = buildElementChoices(elementName, value.isChoiceType(), true); jsCall.append("'").append(CmsEncoder.escape(newElements.toString(), CmsEncoder.ENCODING_UTF_8)).append("'"); jsCall.append(");"); String result; if (buttonPresent) { // at least one button active, create mouseover button String btIcon = "xmledit.png"; String btAction = jsCall.toString(); // determine icon to use and if a direct click action is possible if (addElement && removeElement) { btIcon = "xmledit_del_add.png"; } else if (addElement) { btIcon = "xmledit_add.png"; // create button action to add element on button click StringBuffer action = new StringBuffer(128); action.append("addElement('"); action.append(elementName); action.append("', "); action.append(index); action.append(", '").append(CmsEncoder.escape(newElements.toString(), CmsEncoder.ENCODING_UTF_8)).append( "'"); action.append(");"); btAction = action.toString(); } else if (removeElement) { btIcon = "xmledit_del.png"; // create button action to remove element on button click StringBuffer action = new StringBuffer(128); action.append("removeElement('"); action.append(elementName); action.append("', "); action.append(index); action.append(");"); btAction = action.toString(); } StringBuffer href = new StringBuffer(512); href.append("javascript:"); href.append(btAction); href.append("\" onmouseover=\""); href.append(jsCall); href.append("checkElementButtons(true);\" onmouseout=\"checkElementButtons(false);\" id=\"btimg."); href.append(elementName).append(".").append(index); result = button(href.toString(), null, btIcon, Messages.GUI_EDITOR_XMLCONTENT_ELEMENT_BUTTONS_0, 0); } else { // no active button, create a spacer result = buttonBarSpacer(1); } return result; } /** * Corrects the XML structure of the edited content according to the XSD.<p> * * @throws CmsException if the correction fails */ private void correctXmlStructure() throws CmsException { m_content.setAutoCorrectionEnabled(true); m_content.correctXmlStructure(getCms()); // write the corrected temporary file writeContent(); } /** * Returns the error handler for error handling of the edited xml content.<p> * * @return the error handler */ private CmsXmlContentErrorHandler getValidationHandler() { if (m_validationHandler == null) { // errors were not yet checked, do this now and store result in member m_validationHandler = m_content.validate(getCms()); } return m_validationHandler; } /** * Generates the HTML form for the XML content editor.<p> * * This is a recursive method because nested schemas are possible, * do not call this method directly.<p> * * @param contentDefinition the content definition to start with * @param pathPrefix for nested xml content * @param showHelpBubble if the code for a help bubble should be generated * * @return the HTML that generates the form for the XML editor */ private StringBuffer getXmlEditorForm( CmsXmlContentDefinition contentDefinition, String pathPrefix, boolean showHelpBubble, boolean superTabOpened) { StringBuffer result = new StringBuffer(1024); // only show errors if editor is not opened initially boolean showErrors = (getAction() != ACTION_NEW) && (getAction() != ACTION_DEFAULT) && (getAction() != ACTION_ELEMENT_ADD) && (getAction() != ACTION_ELEMENT_REMOVE) && (getAction() != ACTION_ELEMENT_MOVE_DOWN) && (getAction() != ACTION_ELEMENT_MOVE_UP); try { // check if we are in a nested content definition boolean nested = CmsStringUtil.isNotEmpty(pathPrefix); boolean useTabs = false; boolean tabOpened = false; StringBuffer selectedTabScript = new StringBuffer(64); boolean collapseLabel = false; boolean firstElement = true; // show error header once if there were validation errors if (!nested && showErrors && (getValidationHandler().hasErrors())) { result.append("<div class=\"ui-widget\">"); result.append("<div class=\"ui-state-error ui-corner-all\" style=\"padding: 0pt 0.7em;\"><div style=\"padding: 3px 0;\">"); result.append("<span class=\"ui-icon ui-icon-alert\" style=\"float: left; margin-right: 0.3em;\"></span>"); boolean differentLocaleErrors = false; if ((getValidationHandler().getErrors(getElementLocale()) == null) || (getValidationHandler().getErrors().size() > getValidationHandler().getErrors(getElementLocale()).size())) { differentLocaleErrors = true; result.append("<span id=\"xmlerrordialogbutton\" class=\"ui-icon ui-icon-newwin\" style=\"float: left; margin-right: 0.3em;\"></span>"); } result.append(key(Messages.ERR_EDITOR_XMLCONTENT_VALIDATION_ERROR_TITLE_0)); result.append("</div>"); // show errors in different locales if (differentLocaleErrors) { result.append("<div id=\"xmlerrordialog\" style=\"display: none;\">"); // iterate through all found errors Map<Locale, Map<String, String>> locErrors = getValidationHandler().getErrors(); Iterator<Map.Entry<Locale, Map<String, String>>> locErrorsIter = locErrors.entrySet().iterator(); while (locErrorsIter.hasNext()) { Map.Entry<Locale, Map<String, String>> locEntry = locErrorsIter.next(); Locale locale = locEntry.getKey(); // skip errors in the actual locale if (getElementLocale().equals(locale)) { continue; } result.append("<div style=\"padding: 3px;\"><strong>"); result.append(key( Messages.ERR_EDITOR_XMLCONTENT_VALIDATION_ERROR_LANG_1, new Object[] {locale.getLanguage()})); result.append("</strong></div>\n"); result.append("<ul>"); // iterate through the found errors in a different locale Map<String, String> elErrors = locEntry.getValue(); Iterator<Map.Entry<String, String>> elErrorsIter = elErrors.entrySet().iterator(); while (elErrorsIter.hasNext()) { Map.Entry<String, String> elEntry = elErrorsIter.next(); String nodeName = elEntry.getKey(); String errorMsg = elEntry.getValue(); // output the error message result.append("<li>"); result.append(nodeName); result.append(": "); result.append(errorMsg); result.append("</li>\n"); } result.append("</ul>"); } result.append("</div>\n"); result.append("<script type=\"text/javascript\">\n"); result.append("$(\"#xmlerrordialog\").dialog({\n"); result.append("\tautoOpen: true,\n"); result.append("\tbgiframe: true,\n"); result.append("\twidth: 500,\n"); result.append("\tposition: 'center',\n"); result.append("\tdialogClass: 'ui-state-error',\n"); result.append("\ttitle: '").append(key(Messages.ERR_EDITOR_XMLCONTENT_VALIDATION_ERROR_TITLE_0)).append( "',\n"); result.append("\tmaxHeight: 600\n"); result.append("});\n"); result.append("$(\"#xmlerrordialogbutton\").bind(\"click\", function(e) {$(\"#xmlerrordialog\").dialog(\"open\");});\n"); result.append("</script>"); } result.append("</div></div>"); } if (!nested) { // check if tabs should be shown useTabs = contentDefinition.getContentHandler().getTabs().size() > 0; if (useTabs) { // we have some tabs available, generate them on first level result.append("<div id=\"xmltabs\" class=\"ui-tabs\">\n<ul>\n"); for (Iterator<CmsXmlContentTab> i = contentDefinition.getContentHandler().getTabs().iterator(); i.hasNext();) { CmsXmlContentTab tab = i.next(); result.append("\t<li id=\"OcmsTabTab").append(tab.getIdName()).append("\"><a href=\"#OcmsTab"); result.append(tab.getIdName()); result.append("\"><span>"); result.append(keyDefault(A_CmsWidget.LABEL_PREFIX + contentDefinition.getInnerName() + "." + tab.getTabName(), tab.getTabName())); result.append("</span></a></li>\n"); } result.append("</ul>\n"); } } // if xsd:choice then we just need to get one element of the sequence Iterator<I_CmsXmlSchemaType> it = contentDefinition.getChoiceMaxOccurs() > 0 ? contentDefinition.getTypeSequence().subList(0, 1).iterator() : contentDefinition.getTypeSequence().iterator(); // iterate the type sequence while (it.hasNext()) { // get the type I_CmsXmlSchemaType type = it.next(); boolean tabCurrentlyOpened = false; if (useTabs) { // check if a tab is starting with this element for (int tabIndex = 0; tabIndex < contentDefinition.getContentHandler().getTabs().size(); tabIndex++) { CmsXmlContentTab checkTab = contentDefinition.getContentHandler().getTabs().get(tabIndex); if (checkTab.getStartName().equals(type.getName())) { // a tab is starting, add block element if (tabOpened) { // close a previously opened tab result.append("</table>\n</div>\n"); } result.append("<div id=\"OcmsTab"); result.append(checkTab.getIdName()); result.append("\" class=\"ui-tabs-hide\">\n"); // set necessary values tabOpened = true; tabCurrentlyOpened = true; collapseLabel = checkTab.isCollapsed(); m_currentTab = checkTab; m_currentTabIndex = tabIndex; // leave loop break; } } } if (firstElement || tabCurrentlyOpened) { // create table before first element or if a tab has been opened before result.append("<table class=\"xmlTable"); if (nested && !superTabOpened) { // use other style for nested content definition table if tab was not opened on upper level result.append("Nested"); } result.append("\">\n"); firstElement = false; } // get the element sequence of the current type CmsXmlContentValueSequence elementSequence = m_content.getValueSequence( pathPrefix + type.getName(), getElementLocale()); int elementCount = elementSequence.getElementCount(); // check if value is optional or multiple boolean addValue = elementCount < elementSequence.getMaxOccurs(); boolean removeValue = elementCount > elementSequence.getMinOccurs(); // assure that at least one element is present in sequence boolean disabledElement = false; if ((contentDefinition.getChoiceMaxOccurs() == 0) && (elementCount < 1)) { // current element is disabled, create dummy element if it is a nested type or no choice option elementCount = 1; elementSequence.addValue(getCms(), 0); disabledElement = true; m_optionalElementPresent = true; } // loop through multiple elements for (int j = 0; j < elementCount; j++) { // get value and corresponding widget I_CmsXmlContentValue value = elementSequence.getValue(j); String key = value.getPath(); // check if a tab should be preselected for an added, removed or moved up/down element if ((m_currentTab != null) && CmsStringUtil.isNotEmpty(getParamElementName())) { // an element was modified, add JS to preselect tab if (key.startsWith(getParamElementName()) && (selectedTabScript.length() == 0)) { selectedTabScript.append("<script type=\"text/javascript\">\n\txmlSelectedTab = ").append( m_currentTabIndex).append(";\n</script>\n"); } } // show errors and/or warnings if (showErrors && getValidationHandler().hasErrors(getElementLocale()) && getValidationHandler().getErrors(getElementLocale()).containsKey(key)) { // show error message if (collapseLabel) { result.append("<tr><td class=\"xmlTdError\"><img src=\""); result.append(getEditorResourceUri()); result.append("error.png\" border=\"0\" alt=\"\" align=\"left\" hspace=\"5\">"); result.append(resolveMacros(getValidationHandler().getErrors(getElementLocale()).get(key))); result.append("</td><td></td></tr>\n"); } else { result.append("<tr><td></td><td><img src=\""); result.append(getEditorResourceUri()); result.append("error.png"); result.append("\" border=\"0\" alt=\"\"></td><td class=\"xmlTdError\">"); result.append(resolveMacros(getValidationHandler().getErrors(getElementLocale()).get(key))); result.append("</td><td></td></tr>\n"); } // mark tab as error tab if tab is present String elemName = CmsXmlUtils.getFirstXpathElement(value.getPath()); if (((m_currentTab != null) && !m_errorTabs.contains(m_currentTab)) && (elemName.equals(m_currentTab.getStartName()) || (!CmsXmlUtils.isDeepXpath(value.getPath()) && value.getName().equals( elemName)))) { m_errorTabs.add(m_currentTab); } } // warnings can be additional to errors if (showErrors && getValidationHandler().hasWarnings(getElementLocale()) && getValidationHandler().getWarnings(getElementLocale()).containsKey(key)) { // show warning message if (collapseLabel) { result.append("<tr><td class=\"xmlTdError\"><img src=\""); result.append(getEditorResourceUri()); result.append("warning.png\" border=\"0\" alt=\"\" align=\"left\" hspace=\"5\">"); result.append(resolveMacros(getValidationHandler().getWarnings(getElementLocale()).get(key))); result.append("</td><td></td></tr>\n"); } else { result.append("<tr><td></td><td><img src=\""); result.append(getEditorResourceUri()); result.append("warning.png"); result.append("\" border=\"0\" alt=\"\"></td><td class=\"xmlTdWarning\">"); result.append(resolveMacros(getValidationHandler().getWarnings(getElementLocale()).get(key))); result.append("</td><td></td></tr>\n"); } // mark tab as warning tab if tab is present String elemName = CmsXmlUtils.getFirstXpathElement(value.getPath()); if (((m_currentTab != null) && !m_warningTabs.contains(m_currentTab)) && (elemName.equals(m_currentTab.getStartName()) || (!CmsXmlUtils.isDeepXpath(value.getPath()) && value.getName().equals( elemName)))) { m_warningTabs.add(m_currentTab); } } I_CmsWidget widget = value.isSimpleType() ? contentDefinition.getContentHandler().getWidget(value) : null; int index = value.getIndex(); // create label and help bubble cells result.append("<tr>"); if (!collapseLabel) { result.append("<td class=\"xmlLabel"); if (disabledElement) { // element is disabled, mark it with CSS result.append("Disabled"); } result.append("\">"); result.append(keyDefault(A_CmsWidget.getLabelKey((I_CmsWidgetParameter)value), value.getName())); if (elementCount > 1) { result.append(" [").append(index + 1).append("]"); } result.append(": </td>"); if (showHelpBubble && (widget != null) && (CmsXmlUtils.getXpathIndexInt(value.getPath()) == 1)) { // show help bubble only on first element of each content definition result.append(widget.getHelpBubble(getCms(), this, (I_CmsWidgetParameter)value)); } else { // create empty cell for all following elements result.append(buttonBarSpacer(16)); } } // append individual widget HTML cell if element is enabled if (disabledElement) { // disabled element, show message for optional element result.append("<td class=\"xmlTdDisabled maxwidth\">"); result.append(key(Messages.GUI_EDITOR_XMLCONTENT_OPTIONALELEMENT_0)); result.append("</td>"); } else { // element is enabled, show it if (value.isSimpleType()) { // this is a simple type, display widget result.append(widget.getDialogWidget(getCms(), this, (I_CmsWidgetParameter)value)); } else { // recurse into nested type sequence result.append("<td class=\"maxwidth\">"); boolean showHelp = (j == 0); superTabOpened = !nested && tabOpened && collapseLabel; result.append(getXmlEditorForm( ((CmsXmlNestedContentDefinition)value).getNestedContentDefinition(), value.getPath() + "/", showHelp, superTabOpened)); result.append("</td>"); } } // append element operation (add, remove, move) buttons if required result.append(buildElementButtons(value, addValue, removeValue)); // close row result.append("</tr>\n"); // remove disabled element to avoid eventual side effects, e.g. in widget configurations if (disabledElement) { elementSequence.removeValue(0); } } } // close table result.append("</table>\n"); if (tabOpened) { // close last open tab result.append("</div>\n"); } if (!nested && useTabs) { // close block element around tabs result.append("</div>\n"); // mark eventual warning and error tabs result.append("<script type=\"text/javascript\">\n"); for (Iterator<CmsXmlContentTab> i = m_warningTabs.iterator(); i.hasNext();) { CmsXmlContentTab checkTab = i.next(); if (!m_errorTabs.contains(checkTab)) { result.append("\txmlWarningTabs[xmlWarningTabs.length] = \"").append(checkTab.getIdName()).append( "\";\n"); } } for (Iterator<CmsXmlContentTab> i = m_errorTabs.iterator(); i.hasNext();) { CmsXmlContentTab checkTab = i.next(); result.append("\txmlErrorTabs[xmlErrorTabs.length] = \"").append(checkTab.getIdName()).append( "\";\n"); } result.append("</script>\n"); } if (selectedTabScript.length() > 0) { result.append(selectedTabScript); } } catch (Throwable t) { LOG.error(Messages.get().getBundle().key(Messages.ERR_XML_EDITOR_0), t); } return result; } /** * Resets the error handler member variable to reinitialize the error messages.<p> */ private void resetErrorHandler() { m_validationHandler = null; } /** * Resets the widget collector member variable to reinitialize the widgets.<p> * * This is needed to display the help messages of optional elements before building the html end of the form.<p> */ private void resetWidgetCollector() { m_widgetCollector = null; } /** * Writes the xml content to the vfs and re-initializes the member variables.<p> * * @throws CmsException if writing the file fails */ private void writeContent() throws CmsException { String decodedContent = m_content.toString(); try { m_file.setContents(decodedContent.getBytes(getFileEncoding())); } catch (UnsupportedEncodingException e) { throw new CmsException(Messages.get().container(Messages.ERR_INVALID_CONTENT_ENC_1, getParamResource()), e); } // the file content might have been modified during the write operation m_file = getCloneCms().writeFile(m_file); m_content = CmsXmlContentFactory.unmarshal(getCloneCms(), m_file); } }