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.repository.page; 017 018import java.io.IOException; 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024 025import org.apache.avalon.framework.component.Component; 026import org.apache.avalon.framework.context.Context; 027import org.apache.avalon.framework.context.ContextException; 028import org.apache.avalon.framework.context.Contextualizable; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.avalon.framework.service.Serviceable; 032import org.apache.cocoon.ProcessingException; 033import org.apache.cocoon.components.ContextHelper; 034import org.apache.cocoon.environment.Request; 035import org.apache.commons.lang.StringUtils; 036 037import org.ametys.core.observation.Event; 038import org.ametys.core.observation.ObservationManager; 039import org.ametys.core.ui.Callable; 040import org.ametys.core.user.CurrentUserProvider; 041import org.ametys.plugins.repository.AmetysObjectResolver; 042import org.ametys.plugins.repository.UnknownAmetysObjectException; 043import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 044import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder; 045import org.ametys.plugins.repository.data.holder.group.impl.ModelAwareRepeater; 046import org.ametys.plugins.repository.data.holder.group.impl.ModelAwareRepeaterEntry; 047import org.ametys.plugins.repository.data.holder.group.impl.ModifiableModelAwareRepeater; 048import org.ametys.plugins.repository.data.holder.group.impl.ModifiableModelAwareRepeaterEntry; 049import org.ametys.plugins.repository.model.RepeaterDefinition; 050import org.ametys.runtime.i18n.I18nizableText; 051import org.ametys.runtime.model.DefinitionAndValue; 052import org.ametys.runtime.model.ElementDefinition; 053import org.ametys.runtime.model.ModelHelper; 054import org.ametys.runtime.model.ModelItem; 055import org.ametys.runtime.model.ModelItemContainer; 056import org.ametys.runtime.model.View; 057import org.ametys.runtime.model.type.ElementType; 058import org.ametys.runtime.plugin.component.AbstractLogEnabled; 059import org.ametys.web.ObservationConstants; 060import org.ametys.web.WebConstants; 061import org.ametys.web.repository.page.Page.PageType; 062import org.ametys.web.repository.page.ZoneItem.ZoneType; 063import org.ametys.web.service.Service; 064import org.ametys.web.service.ServiceExtensionPoint; 065 066/** 067 * Class containing callables to retrieve and configure service parameters 068 */ 069public class ZoneItemManager extends AbstractLogEnabled implements Component, Serviceable, Contextualizable 070{ 071 /** Avalon Role */ 072 public static final String ROLE = ZoneItemManager.class.getName(); 073 074 /** Constant for untouched binary metadata. */ 075 protected static final String __SERVICE_PARAM_UNTOUCHED_BINARY = "untouched"; 076 077 private ServiceExtensionPoint _serviceExtensionPoint; 078 private AmetysObjectResolver _resolver; 079 private ObservationManager _observationManager; 080 private CurrentUserProvider _currentUserProvider; 081 private Context _context; 082 083 public void service(ServiceManager manager) throws ServiceException 084 { 085 _serviceExtensionPoint = (ServiceExtensionPoint) manager.lookup(ServiceExtensionPoint.ROLE); 086 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 087 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 088 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 089 } 090 091 public void contextualize(Context context) throws ContextException 092 { 093 _context = context; 094 } 095 096 /** 097 * Retrieves the parameter definitions of the given service 098 * @param serviceId Identifier of the service 099 * @param pageId the page id 100 * @param zoneItemId the zone item id 101 * @param zoneName the zone name 102 * @return the parameter definitions 103 * @throws ProcessingException if an error occurs 104 */ 105 @Callable 106 public Map<String, Object> getServiceParameterDefinitions(String serviceId, String pageId, String zoneItemId, String zoneName) throws ProcessingException 107 { 108 _setRequestAttribute(serviceId, pageId, zoneItemId, zoneName); 109 110 Map<String, Object> response = new HashMap<>(); 111 112 Service service = _serviceExtensionPoint.getExtension(serviceId); 113 114 response.put("id", serviceId); 115 response.put("url", service.getURL()); 116 response.put("label", service.getLabel()); 117 response.put("height", service.getCreationBoxHeight()); 118 response.put("width", service.getCreationBoxWidth()); 119 120 View serviceView = service.getView(); 121 response.put("parameters", serviceView.toJSON()); 122 123 return response; 124 } 125 126 private void _setRequestAttribute(String serviceId, String pageId, String zoneItemId, String zoneName) 127 { 128 Request request = ContextHelper.getRequest(_context); 129 130 request.setAttribute(WebConstants.REQUEST_ATTR_SERVICE_ID, serviceId); 131 request.setAttribute(WebConstants.REQUEST_ATTR_PAGE_ID, pageId); 132 request.setAttribute(WebConstants.REQUEST_ATTR_ZONEITEM_ID, zoneItemId); 133 request.setAttribute(WebConstants.REQUEST_ATTR_ZONE_NAME, zoneName); 134 } 135 136 /** 137 * Get the service parameter values 138 * @param zoneItemId the zone item id 139 * @param serviceId the service Id 140 * @return the values 141 */ 142 @Callable 143 public Map<String, Object> getServiceParameterValues(String zoneItemId, String serviceId) 144 { 145 Service service = _serviceExtensionPoint.getExtension(serviceId); 146 ZoneItem zoneItem = _resolver.resolveById(zoneItemId); 147 148 Map<String, Object> response = new HashMap<>(); 149 Collection<ModelItem> serviceParameterDefinitions = service.getParameters().values(); 150 ModelAwareDataHolder dataHolder = zoneItem.getServiceParameters(); 151 152 Map<String, Object> values = _getValues(serviceParameterDefinitions, dataHolder, ""); 153 response.put("values", values); 154 155 List<Map<String, Object>> repeaters = _getRepeaters(serviceParameterDefinitions, dataHolder, ""); 156 response.put("repeaters", repeaters); 157 158 return response; 159 } 160 161 private Map<String, Object> _getValues(Collection<ModelItem> items, ModelAwareDataHolder dataHolder, String prefix) 162 { 163 Map<String, Object> values = new HashMap<>(); 164 165 for (ModelItem item : items) 166 { 167 if (item instanceof ElementDefinition) 168 { 169 if (dataHolder.hasValue(item.getName())) 170 { 171 ElementType type = ((ElementDefinition) item).getType(); 172 Object value = dataHolder.getValue(item.getName()); 173 174 values.put(prefix + item.getName(), type.valueToJSONForClient(value)); 175 } 176 } 177 else if (item instanceof RepeaterDefinition) 178 { 179 if (dataHolder.hasValue(item.getName())) 180 { 181 ModelAwareRepeater repeater = dataHolder.getRepeater(item.getName()); 182 for (ModelAwareRepeaterEntry entry: repeater.getEntries()) 183 { 184 String newPrefix = prefix + item.getName() + "[" + entry.getPosition() + "]/"; 185 values.putAll(_getValues(((RepeaterDefinition) item).getChildren(), entry, newPrefix)); 186 } 187 values.put(prefix + item.getName(), new ArrayList<>()); 188 } 189 } 190 } 191 192 return values; 193 } 194 195 private List<Map<String, Object>> _getRepeaters(Collection<ModelItem> items, ModelAwareDataHolder dataHolder, String prefix) 196 { 197 List<Map<String, Object>> results = new ArrayList<>(); 198 199 for (ModelItem item : items) 200 { 201 if (item instanceof RepeaterDefinition) 202 { 203 Map<String, Object> result = new HashMap<>(); 204 205 result.put("name", item.getName()); 206 result.put("prefix", prefix); 207 208 if (dataHolder.hasValue(item.getName())) 209 { 210 ModelAwareRepeater repeater = dataHolder.getRepeater(item.getName()); 211 result.put("count", repeater.getSize()); 212 for (ModelAwareRepeaterEntry entry: repeater.getEntries()) 213 { 214 StringBuilder newPrefix = new StringBuilder(); 215 newPrefix.append(prefix); 216 newPrefix.append(item.getName()).append("[").append(entry.getPosition()).append("]/"); 217 results.addAll(_getRepeaters(((RepeaterDefinition) item).getChildren(), entry, newPrefix.toString())); 218 } 219 } 220 else 221 { 222 result.put("count", 0); 223 } 224 225 results.add(result); 226 } 227 } 228 229 return results; 230 } 231 232 /** 233 * Add the service to the given zone on given page 234 * @param pageId The page identifier 235 * @param zoneName The zone name 236 * @param serviceId The identifier of the service to add 237 * @param parameterValues the service parameter values. Can be empty 238 * @return The result with the identifiers of updated page, zone and zone item 239 * @throws IOException if an error occurred while saving parameters 240 */ 241 @Callable 242 public Map<String, Object> addService(String pageId, String zoneName, String serviceId, Map<String, Object> parameterValues) throws IOException 243 { 244 if (StringUtils.isEmpty(serviceId) || StringUtils.isEmpty(pageId) || StringUtils.isEmpty(zoneName)) 245 { 246 throw new IllegalArgumentException("ServiceId, PageId or ZoneName is missing"); 247 } 248 249 // Check the service 250 Service service = null; 251 try 252 { 253 service = _serviceExtensionPoint.getExtension(serviceId); 254 } 255 catch (IllegalArgumentException e) 256 { 257 throw new IllegalArgumentException("Service with id '" + serviceId + "' does not exist", e); 258 } 259 260 try 261 { 262 Page page = _resolver.resolveById(pageId); 263 if (!(page instanceof ModifiablePage)) 264 { 265 throw new IllegalArgumentException("Can not affect service on a non-modifiable page " + pageId); 266 } 267 268 ModifiablePage modifiablePage = (ModifiablePage) page; 269 270 if (page.getType() != PageType.CONTAINER) 271 { 272 throw new IllegalArgumentException("Can not affect service on a non-container page " + pageId); 273 } 274 275 ModifiableZone zone; 276 if (page.hasZone(zoneName)) 277 { 278 zone = modifiablePage.getZone(zoneName); 279 } 280 else 281 { 282 zone = modifiablePage.createZone(zoneName); 283 } 284 285 ModifiableZoneItem zoneItem = zone.addZoneItem(); 286 zoneItem.setType(ZoneType.SERVICE); 287 zoneItem.setServiceId(serviceId); 288 289 ModifiableModelAwareDataHolder serviceDataHolder = zoneItem.getServiceParameters(); 290 291 Map<String, List<I18nizableText>> allErrors = new HashMap<>(); 292 _setParameterValues(serviceDataHolder, service, parameterValues, allErrors); 293 294 Map<String, Object> results = new HashMap<>(); 295 if (!allErrors.isEmpty()) 296 { 297 results.put("errors", allErrors); 298 return results; 299 } 300 301 modifiablePage.saveChanges(); 302 303 Map<String, Object> eventParams = new HashMap<>(); 304 eventParams.put(ObservationConstants.ARGS_PAGE, page); 305 eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItem.getId()); 306 eventParams.put(ObservationConstants.ARGS_ZONE_TYPE, ZoneType.SERVICE); 307 _observationManager.notify(new Event(ObservationConstants.EVENT_ZONEITEM_ADDED, _currentUserProvider.getUser(), eventParams)); 308 309 results.put("id", page.getId()); 310 results.put("zoneitem-id", zoneItem.getId()); 311 results.put("zone-name", zone.getName()); 312 313 return results; 314 } 315 catch (UnknownAmetysObjectException e) 316 { 317 throw new IllegalArgumentException("An error occured adding the service '" + serviceId + "' on the page '" + pageId + "'", e); 318 } 319 } 320 321 /** 322 * Edit the parameter values of the given service 323 * @param zoneItemId The identifier of the zone item holding the service 324 * @param serviceId The service identifier 325 * @param parameterValues the service parameter values to update 326 * @return The result with the identifiers of updated page, zone and zone item 327 * @throws IOException if an error occurs while saving parameters 328 */ 329 @Callable 330 public Map<String, Object> editServiceParameterValues(String zoneItemId, String serviceId, Map<String, Object> parameterValues) throws IOException 331 { 332 Service service = null; 333 try 334 { 335 service = _serviceExtensionPoint.getExtension(serviceId); 336 } 337 catch (IllegalArgumentException e) 338 { 339 throw new IllegalArgumentException("Service with id '" + serviceId + "' does not exist", e); 340 } 341 342 ZoneItem zoneItem = _resolver.resolveById(zoneItemId); 343 if (!(zoneItem instanceof ModifiableZoneItem)) 344 { 345 throw new IllegalArgumentException("Can not configure service on a non-modifiable zone item " + zoneItemId); 346 } 347 348 ModifiableZoneItem modifiableZoneItem = (ModifiableZoneItem) zoneItem; 349 350 ModifiableModelAwareDataHolder serviceDataHolder = modifiableZoneItem.getServiceParameters(); 351 352 Map<String, List<I18nizableText>> allErrors = new HashMap<>(); 353 _setParameterValues(serviceDataHolder, service, parameterValues, allErrors); 354 355 Map<String, Object> results = new HashMap<>(); 356 if (!allErrors.isEmpty()) 357 { 358 results.put("errors", allErrors); 359 return results; 360 } 361 362 modifiableZoneItem.saveChanges(); 363 364 Map<String, Object> eventParams = new HashMap<>(); 365 eventParams.put(ObservationConstants.ARGS_ZONE_ITEM, zoneItem); 366 eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItem.getId()); 367 _observationManager.notify(new Event(ObservationConstants.EVENT_SERVICE_MODIFIED, _currentUserProvider.getUser(), eventParams)); 368 369 results.put("id", zoneItem.getZone().getPage().getId()); 370 results.put("zoneitem-id", zoneItem.getId()); 371 results.put("zone-name", zoneItem.getZone().getName()); 372 373 return results; 374 } 375 376 void _setParameterValues(ModifiableModelAwareDataHolder serviceDataHolder, Service service, Map<String, Object> values, Map<String, List<I18nizableText>> allErrors) 377 { 378 Map<String, ModelItem> definitions = service.getParameters(); 379 for (ModelItem definition : definitions.values()) 380 { 381 _getAndSaveParameter(definition, values, serviceDataHolder, "", allErrors); 382 } 383 } 384 385 private void _getAndSaveParameter(ModelItem def, Map<String, Object> values, ModifiableModelAwareDataHolder dataHolder, String prefix, Map<String, List<I18nizableText>> allErrors) 386 { 387 if (def instanceof ElementDefinition) 388 { 389 ElementDefinition definition = (ElementDefinition) def; 390 Map<String, DefinitionAndValue> definitionAndValues = _getBrothersDefinitionAndValues(prefix, values, definition); 391 boolean isGroupSwitchOn = ModelHelper.isGroupSwitchOn(definition, values); 392 boolean isDisabled = ModelHelper.evaluateDisableConditions(definition.getDisableConditions(), definitionAndValues, getLogger()); 393 394 if (isGroupSwitchOn && !isDisabled) 395 { 396 397 Object submittedValue = values.get(prefix + definition.getName()); 398 ElementType parameterType = definition.getType(); 399 Object value = parameterType.fromJSONForClient(submittedValue); 400 401 // TODO RUNTIME-2897: call the validateValue without boolean when multiple values are managed in enumerators 402 List<I18nizableText> errors = ModelHelper.validateValue(definition, value, false); 403 if (!errors.isEmpty()) 404 { 405 allErrors.put(definition.getName(), errors); 406 return; 407 } 408 409 if (!__SERVICE_PARAM_UNTOUCHED_BINARY.equals(value)) 410 { 411 dataHolder.setValue(definition.getName(), value); 412 } 413 } 414 } 415 else if (def instanceof RepeaterDefinition) 416 { 417 RepeaterDefinition definition = (RepeaterDefinition) def; 418 ModifiableModelAwareRepeater repeater = dataHolder.getRepeater(definition.getName(), true); 419 420 // First move the entries according to the given previous positions 421 repeater.moveEntries(_getRepeaterPositionsMapping(values, prefix + definition.getName())); 422 423 // Then save the entries' parameter values 424 for (ModifiableModelAwareRepeaterEntry entry : repeater.getEntries()) 425 { 426 List<ModelItem> childrenDefinitions = definition.getChildren(); 427 String newPrefix = prefix + definition.getName() + "[" + entry.getPosition() + "]" + ModelItem.ITEM_PATH_SEPARATOR; 428 for (ModelItem childDefinition : childrenDefinitions) 429 { 430 _getAndSaveParameter(childDefinition, values, entry, newPrefix, allErrors); 431 } 432 } 433 } 434 } 435 436 /** 437 * Get the definition and value pairs of the given definition brothers. The definition and value pairs are indexed by the parameter name 438 * @param prefix prefix to get the parameter values 439 * @param values all the parameter values 440 * @param definition the definition 441 * @return the definition and value pairs of the given definition brother 442 */ 443 private Map<String, DefinitionAndValue> _getBrothersDefinitionAndValues(String prefix, Map<String, Object> values, ElementDefinition definition) 444 { 445 Map<String, DefinitionAndValue> definitionAndValues = new HashMap<>(); 446 447 ModelItemContainer definitionContainer = definition.getParent() != null ? definition.getParent() : definition.getModel(); 448 Collection< ? extends ModelItem> definitionBrothers; 449 definitionBrothers = definitionContainer.getModelItems(); 450 for (ModelItem brother : definitionBrothers) 451 { 452 Object value = values.get(prefix + brother.getName()); 453 if (brother instanceof ElementDefinition) 454 { 455 DefinitionAndValue definitionAndValue = new DefinitionAndValue<>(null, brother, value); 456 definitionAndValues.put(brother.getName(), definitionAndValue); 457 } 458 } 459 460 return definitionAndValues; 461 } 462 463 private Map<Integer, Integer> _getRepeaterPositionsMapping(Map<String, Object> values, String repeaterPath) 464 { 465 Map<Integer, Integer> positionsMapping = new HashMap<>(); 466 467 String sizeEntryName = "_" + repeaterPath + ModelItem.ITEM_PATH_SEPARATOR + "size"; 468 if (values.containsKey(sizeEntryName)) 469 { 470 int size = (int) values.get(sizeEntryName); 471 for (int position = 1; position <= size; position++) 472 { 473 String previousPositionEntryName = "_" + repeaterPath + "[" + position + "]" + ModelItem.ITEM_PATH_SEPARATOR + "previous-position"; 474 if (values.containsKey(previousPositionEntryName)) 475 { 476 int previousPosition = (int) values.get("_" + repeaterPath + "[" + position + "]" + ModelItem.ITEM_PATH_SEPARATOR + "previous-position"); 477 positionsMapping.put(position, previousPosition); 478 } 479 else 480 { 481 throw new IllegalArgumentException("The given values don't contain the previous position of the repeater entry '" + repeaterPath + "[" + position + "]" + "'."); 482 } 483 } 484 485 return positionsMapping; 486 } 487 else 488 { 489 throw new IllegalArgumentException("The given values don't contain the size of the repeater at path '" + repeaterPath + "'."); 490 } 491 } 492}