001/* 002 * Copyright 2018 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.web.content; 017 018import java.util.HashMap; 019import java.util.LinkedHashMap; 020import java.util.List; 021import java.util.Map; 022import java.util.Map.Entry; 023import java.util.stream.Collectors; 024 025import org.apache.avalon.framework.context.Context; 026import org.apache.avalon.framework.context.ContextException; 027import org.apache.avalon.framework.context.Contextualizable; 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.cocoon.components.ContextHelper; 031import org.apache.cocoon.environment.Request; 032import org.apache.cocoon.xml.AttributesImpl; 033import org.apache.cocoon.xml.XMLUtils; 034import org.apache.commons.collections.MapUtils; 035import org.apache.commons.lang3.ArrayUtils; 036import org.apache.commons.lang3.LocaleUtils; 037import org.apache.commons.lang3.StringUtils; 038import org.xml.sax.ContentHandler; 039import org.xml.sax.SAXException; 040 041import org.ametys.cms.contenttype.ContentAttributeDefinition; 042import org.ametys.cms.contenttype.ContentType; 043import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 044import org.ametys.cms.data.type.ModelItemTypeConstants; 045import org.ametys.cms.repository.Content; 046import org.ametys.cms.search.query.AndQuery; 047import org.ametys.cms.search.query.ContentLanguageQuery; 048import org.ametys.cms.search.query.ContentTypeQuery; 049import org.ametys.cms.search.query.DocumentTypeQuery; 050import org.ametys.cms.search.query.Query; 051import org.ametys.cms.search.solr.SearcherFactory.Searcher; 052import org.ametys.cms.workflow.ContentWorkflowHelper; 053import org.ametys.cms.workflow.CreateContentFunction; 054import org.ametys.cms.workflow.EditContentFunction; 055import org.ametys.plugins.repository.AmetysObjectIterable; 056import org.ametys.plugins.repository.AmetysRepositoryException; 057import org.ametys.plugins.repository.RepositoryConstants; 058import org.ametys.plugins.repository.jcr.NameHelper.NameComputationMode; 059import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector; 060import org.ametys.plugins.workflow.AbstractWorkflowComponent; 061import org.ametys.runtime.i18n.I18nizableText; 062import org.ametys.runtime.model.ModelHelper; 063import org.ametys.web.FOAmetysObjectCreationHelper; 064import org.ametys.web.frontoffice.FrontOfficeSearcherFactory; 065 066import com.google.common.collect.Multimap; 067import com.opensymphony.workflow.WorkflowException; 068 069/** 070 * Helper for creating and editing a content from the submitted form 071 */ 072public class FOContentCreationHelper extends FOAmetysObjectCreationHelper implements Contextualizable 073{ 074 /** The component role. */ 075 @SuppressWarnings("hiding") 076 public static final String ROLE = FOContentCreationHelper.class.getName(); 077 078 private ContentWorkflowHelper _contentWorkflowHelper; 079 080 private Context _context; 081 082 private ContentTypeExtensionPoint _cTypeExtPt; 083 084 private FrontOfficeSearcherFactory _searcherFactory; 085 086 @Override 087 public void service(ServiceManager smanager) throws ServiceException 088 { 089 super.service(smanager); 090 _cTypeExtPt = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE); 091 _contentWorkflowHelper = (ContentWorkflowHelper) smanager.lookup(ContentWorkflowHelper.ROLE); 092 _searcherFactory = (FrontOfficeSearcherFactory) smanager.lookup(FrontOfficeSearcherFactory.ROLE); 093 } 094 095 public void contextualize(Context context) throws ContextException 096 { 097 _context = context; 098 } 099 100 /** 101 * Get the request 102 * @return the request 103 */ 104 protected Request _getRequest() 105 { 106 return ContextHelper.getRequest(_context); 107 } 108 109 /** 110 * SAX contents values for metadata of type CONTENT 111 * @param contentHandler The content handler to sax into 112 * @param contentType The content type 113 * @param rootTagName The root tag name 114 * @param language the current language 115 * @throws SAXException if an error occurs when saxing 116 */ 117 public void saxContentValues(ContentHandler contentHandler, ContentType contentType, String rootTagName, String language) throws SAXException 118 { 119 List<ContentAttributeDefinition> contentAttributes = ModelHelper.findModelItemsByType(contentType, ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID) 120 .stream() 121 .filter(ContentAttributeDefinition.class::isInstance) // Avoid properties, that are not modifiables 122 .map(ContentAttributeDefinition.class::cast) 123 .collect(Collectors.toList()); 124 for (ContentAttributeDefinition contentAttribute : contentAttributes) 125 { 126 AttributesImpl attrs = new AttributesImpl(); 127 attrs.addCDATAAttribute("name", contentAttribute.getPath().replace("/", ".")); 128 XMLUtils.startElement(contentHandler, rootTagName, attrs); 129 _saxContentEnumeratorValue(contentHandler, contentAttribute, language); 130 XMLUtils.endElement(contentHandler, rootTagName); 131 } 132 } 133 134 135 /** 136 * Sax enumeration value for enum or an attribute of type content 137 * @param contentHandler The content handler to sax into 138 * @param attribute The attribute of type content 139 * @param language The current language 140 * @throws SAXException If an error occurred while saxing 141 */ 142 private void _saxContentEnumeratorValue(ContentHandler contentHandler, ContentAttributeDefinition attribute, String language) throws SAXException 143 { 144 Map<String, String> values = getContentValues(attribute.getContentTypeId(), language); 145 146 XMLUtils.startElement(contentHandler, "enumeration"); 147 for (Entry<String, String> entry : values.entrySet()) 148 { 149 AttributesImpl attrItem = new AttributesImpl(); 150 attrItem.addCDATAAttribute("value", entry.getKey()); 151 XMLUtils.startElement(contentHandler, "item", attrItem); 152 XMLUtils.createElement(contentHandler, "label", entry.getValue()); 153 XMLUtils.endElement(contentHandler, "item"); 154 } 155 XMLUtils.endElement(contentHandler, "enumeration"); 156 } 157 158 /** 159 * Get values for contents enumeration 160 * @param cTypeId The id of content type 161 * @param language The current language 162 * @return The contents 163 */ 164 protected Map<String, String> getContentValues(String cTypeId, String language) 165 { 166 try 167 { 168 Query query = null; 169 boolean multilingual = false; 170 if (StringUtils.isNotEmpty(cTypeId)) 171 { 172 query = new ContentTypeQuery(cTypeId); 173 multilingual = _cTypeExtPt.getExtension(cTypeId).isMultilingual(); 174 } 175 176 if (!multilingual) 177 { 178 query = query != null ? new AndQuery(query, new ContentLanguageQuery(language)) : new ContentLanguageQuery(language); 179 } 180 181 Searcher searcher = _searcherFactory.create().withQuery(query) 182 .addFilterQuery(new DocumentTypeQuery("content")) 183 .withLimits(0, Integer.MAX_VALUE); 184 185 AmetysObjectIterable<Content> contents = searcher.search(); 186 187 return contents.stream() 188 .collect(Collectors.toMap(Content::getId, c -> c.getTitle(LocaleUtils.toLocale(language)))) 189 .entrySet() 190 .stream() 191 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); 192 } 193 catch (Exception e) 194 { 195 getLogger().error("Failed to get content enumeration for content type " + cTypeId, e); 196 return MapUtils.EMPTY_MAP; 197 } 198 } 199 200 /** 201 * Get the values for this content type from request 202 * @param request the request 203 * @param contentType the edited content type 204 * @param viewName the view name 205 * @param errors The errors to fill 206 * @return The values 207 */ 208 public Map<String, Object> getAndValidateFormValues(Request request, ContentType contentType, String viewName, Multimap<String, I18nizableText> errors) 209 { 210 Map<String, Object> values = getFormValues(request, contentType.getView(viewName), StringUtils.EMPTY, errors); 211 errors.putAll(validateValues(values, contentType.getView(viewName))); 212 return values; 213 } 214 215 /** 216 * Create and edit a content 217 * @param initActionId The initial workflow action id for creation and edition 218 * @param contentTypeId The id of content type 219 * @param siteName The current site name 220 * @param contentName The content name 221 * @param contentTitle The content title 222 * @param language The content language 223 * @param values The submitted values 224 * @param workflowName The workflow name 225 * @param viewName The view name 226 * @return The workflow result 227 * @throws AmetysRepositoryException if an error occurs 228 * @throws WorkflowException if an error occurs 229 */ 230 public Map<String, Object> createAndEditContent(int initActionId, String contentTypeId, String siteName, String contentName, String contentTitle, String language, Map<String, Object> values, String workflowName, String viewName) throws AmetysRepositoryException, WorkflowException 231 { 232 return createAndEditContent(initActionId, contentTypeId, siteName, contentName, contentTitle, language, values, workflowName, viewName, new HashMap<>()); 233 } 234 235 /** 236 * Create and edit a content 237 * @param initActionId The initial workflow action id for creation and edition 238 * @param contentTypeId The id of content type 239 * @param siteName The current site name 240 * @param contentName The content name 241 * @param contentTitle The content title 242 * @param language The content language 243 * @param values The submitted values 244 * @param workflowName The workflow name 245 * @param viewName The view name 246 * @param inputs The initial workflow inputs 247 * @return The workflow result 248 * @throws AmetysRepositoryException if an error occurs 249 * @throws WorkflowException if an error occurs 250 */ 251 public Map<String, Object> createAndEditContent(int initActionId, String contentTypeId, String siteName, String contentName, String contentTitle, String language, Map<String, Object> values, String workflowName, String viewName, Map<String, Object> inputs) throws AmetysRepositoryException, WorkflowException 252 { 253 return createAndEditContent(initActionId, new String[] {contentTypeId}, ArrayUtils.EMPTY_STRING_ARRAY, siteName, contentName, contentTitle, language, values, workflowName, viewName, inputs); 254 } 255 256 /** 257 * Create and edit a content 258 * @param initActionId The initial workflow action id for creation and edition 259 * @param contentTypeIds The new content types. Cannot be null. Cannot be empty. 260 * @param mixinIds The new mixins. Can be null. Can be empty. 261 * @param siteName The current site name 262 * @param contentName The content name 263 * @param contentTitle The content title 264 * @param language The content language 265 * @param values The values of the content attributes 266 * @param workflowName The workflow name 267 * @param viewName The view name 268 * @param inputs The initial workflow inputs 269 * @return The workflow result 270 * @throws AmetysRepositoryException if an error occurs 271 * @throws WorkflowException if an error occurs 272 */ 273 public Map<String, Object> createAndEditContent(int initActionId, String[] contentTypeIds, String[] mixinIds, String siteName, String contentName, String contentTitle, String language, Map<String, Object> values, String workflowName, String viewName, Map<String, Object> inputs) throws AmetysRepositoryException, WorkflowException 274 { 275 Request request = _getRequest(); 276 277 // Retrieve the current workspace. 278 String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 279 280 try 281 { 282 // Force the default workspace. 283 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, RepositoryConstants.DEFAULT_WORKSPACE); 284 285 // Workflow parameters. 286 inputs.put(org.ametys.web.workflow.CreateContentFunction.SITE_KEY, siteName); 287 288 Map<String, Object> contextParameters = new HashMap<>(); 289 contextParameters.put(EditContentFunction.QUIT, true); 290 contextParameters.put(EditContentFunction.VALUES_KEY, values); 291 if (viewName != null) 292 { 293 contextParameters.put(EditContentFunction.VIEW_NAME, viewName); 294 } 295 296 inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters); 297 // In FO mode, we always have the name as the default title, to avoid conflicts, we force generated key on content name 298 inputs.put(CreateContentFunction.NAME_COMPUTATION_MODE_KEY, NameComputationMode.GENERATED_KEY.toString()); 299 300 return _contentWorkflowHelper.createContent(workflowName, initActionId, contentName, contentTitle, contentTypeIds, mixinIds, language, inputs); 301 } 302 finally 303 { 304 // Restore context 305 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp); 306 } 307 308 } 309}