001/* 002 * Copyright 2013 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.cms.content.autosave; 017 018import java.time.ZonedDateTime; 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.HashMap; 022import java.util.Iterator; 023import java.util.List; 024import java.util.Map; 025 026import org.apache.avalon.framework.component.Component; 027import org.apache.avalon.framework.logger.AbstractLogEnabled; 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.avalon.framework.service.Serviceable; 031import org.apache.commons.lang3.StringUtils; 032 033import org.ametys.cms.FilterNameHelper; 034import org.ametys.core.ui.Callable; 035import org.ametys.core.user.CurrentUserProvider; 036import org.ametys.core.user.UserIdentity; 037import org.ametys.core.util.DateUtils; 038import org.ametys.core.util.JSONUtils; 039import org.ametys.plugins.repository.AmetysObjectIterable; 040import org.ametys.plugins.repository.AmetysObjectResolver; 041import org.ametys.plugins.repository.ModifiableAmetysObject; 042import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 043import org.ametys.plugins.repository.RemovableAmetysObject; 044import org.ametys.plugins.repository.RepositoryConstants; 045import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 046import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder; 047import org.ametys.plugins.repository.data.holder.group.impl.ModelAwareRepeater; 048import org.ametys.plugins.repository.data.holder.group.impl.ModelAwareRepeaterEntry; 049import org.ametys.plugins.repository.data.holder.group.impl.ModifiableModelAwareRepeater; 050import org.ametys.plugins.repository.data.holder.group.impl.ModifiableModelAwareRepeaterEntry; 051import org.ametys.plugins.repository.jcr.ModelAwareJCRAmetysObject; 052import org.ametys.plugins.repository.query.QueryHelper; 053import org.ametys.plugins.repository.query.expression.Expression; 054import org.ametys.plugins.repository.query.expression.Expression.Operator; 055import org.ametys.plugins.repository.query.expression.StringExpression; 056import org.ametys.runtime.config.Config; 057 058/** 059 * Component for manipulating auto-backup on contents 060 * 061 */ 062public class ContentBackupClientInteraction extends AbstractLogEnabled implements Serviceable, Component 063{ 064 private static final String __AUTOSAVE_NODETYPE = RepositoryConstants.NAMESPACE_PREFIX + ":autoSave"; 065 066 private AmetysObjectResolver _resolver; 067 private CurrentUserProvider _currentUserProvider; 068 private JSONUtils _jsonUtils; 069 070 @Override 071 public void service(ServiceManager manager) throws ServiceException 072 { 073 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 074 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 075 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 076 } 077 078 /** 079 * Get the content backup information 080 * @param contentId The content id 081 * @return The saved data 082 */ 083 @Callable 084 public Map<String, Object> getContentBackup (String contentId) 085 { 086 if (getLogger().isDebugEnabled()) 087 { 088 getLogger().debug(String.format("Get automatic data backup for content '%s'", contentId)); 089 } 090 091 Map<String, Object> result = new HashMap<>(); 092 093 boolean autoSaveEnabled = Config.getInstance().getValue("automatic.save.enabled"); 094 Long autoSaveFrequency = Config.getInstance().getValue("automatic.save.frequency"); 095 096 result.put("enabled", autoSaveEnabled); 097 result.put("frequency", autoSaveFrequency); 098 099 ModelAwareDataHolder contentNode = getContentNode(contentId, false); 100 101 if (contentNode != null) 102 { 103 Map<String, Object> autoBackup = new HashMap<>(); 104 105 UserIdentity creator = contentNode.getValue(ContentBackupAmetysObject.AUTOSAVE_CREATOR); 106 autoBackup.put("creator", creator != null ? creator.toString() : StringUtils.EMPTY); 107 autoBackup.put("contentId", contentId); 108 109 ZonedDateTime backupDate = contentNode.getValue(ContentBackupAmetysObject.AUTOSAVE_TEMP_DATE); 110 if (backupDate != null) 111 { 112 autoBackup.put("date", DateUtils.zonedDateTimeToString(backupDate)); 113 } 114 115 autoBackup.put("data", getBackupData(contentNode)); 116 117 result.put("auto-backup", autoBackup); 118 } 119 120 return result; 121 } 122 123 /** 124 * Retrieves the content backup information 125 * @param dataHolder the content backup data holder 126 * @return the content backup information 127 */ 128 protected Map<String, Object> getBackupData (ModelAwareDataHolder dataHolder) 129 { 130 Map<String, Object> data = new HashMap<>(); 131 132 // Get and generate the valid attributes values. 133 if (dataHolder.hasValue(ContentBackupAmetysObject.AUTOSAVE_VALUES)) 134 { 135 List<Map<String, Object>> values = getValuesBackupData(dataHolder, ContentBackupAmetysObject.AUTOSAVE_VALUES); 136 data.put("attributes", values); 137 } 138 139 // Get and generate the invalid attributes values. 140 if (dataHolder.hasValue(ContentBackupAmetysObject.AUTOSAVE_INVALID_VALUES)) 141 { 142 List<Map<String, Object>> invalidValues = getValuesBackupData(dataHolder, ContentBackupAmetysObject.AUTOSAVE_INVALID_VALUES); 143 data.put("invalid-attributes", invalidValues); 144 } 145 146 // Get the field comments 147 data.put("comments", dataHolder.getValue(ContentBackupAmetysObject.AUTOSAVE_COMMENTS, false, "{}")); 148 149 // Get and generate the repeater item counts. 150 if (dataHolder.hasValue(ContentBackupAmetysObject.AUTOSAVE_REPEATERS)) 151 { 152 List<Map<String, Object>> repeaters = getRepeatersBackupData(dataHolder); 153 data.put("repeaters", repeaters); 154 } 155 156 return data; 157 } 158 159 /** 160 * Retrieves the values backup information 161 * @param dataHolder the content backup data holder 162 * @param valuesRepeaterPath the path of the values attribute 163 * @return the values backup information 164 */ 165 protected List<Map<String, Object>> getValuesBackupData(ModelAwareDataHolder dataHolder, String valuesRepeaterPath) 166 { 167 List<Map<String, Object>> attributes = new ArrayList<>(); 168 169 ModelAwareRepeater values = dataHolder.getRepeater(valuesRepeaterPath); 170 for (ModelAwareRepeaterEntry entry : values.getEntries()) 171 { 172 Map<String, Object> attribute = new HashMap<>(); 173 174 attribute.put("name", entry.getValue(ContentBackupAmetysObject.ATTRIBUTE_NAME, false, StringUtils.EMPTY)); 175 176 String encodedValue = entry.getValue(ContentBackupAmetysObject.ATTRIBUTE_VALUE, false, StringUtils.EMPTY); 177 attribute.put("value", _decodeValue(encodedValue)); 178 179 attributes.add(attribute); 180 } 181 182 return attributes; 183 } 184 185 private Object _decodeValue (String value) 186 { 187 if (StringUtils.isBlank(value)) 188 { 189 return StringUtils.EMPTY; 190 } 191 192 try 193 { 194 return _jsonUtils.convertJsonToMap(value); 195 } 196 catch (IllegalArgumentException e) 197 { 198 // Failed to convert into map, continue 199 } 200 201 try 202 { 203 return _jsonUtils.convertJsonToList(value); 204 } 205 catch (IllegalArgumentException e) 206 { 207 // Failed to convert into list, continue 208 } 209 210 try 211 { 212 return _jsonUtils.convertJsonToArray(value); 213 } 214 catch (IllegalArgumentException e) 215 { 216 // Failed to convert into array, continue 217 } 218 219 return value; 220 } 221 222 /** 223 * Retrieves the repeaters backup information 224 * @param dataHolder the content backup data holder 225 * @return the repeaters backup information 226 */ 227 protected List<Map<String, Object>> getRepeatersBackupData(ModelAwareDataHolder dataHolder) 228 { 229 List<Map<String, Object>> repeaters = new ArrayList<>(); 230 231 ModelAwareRepeater repeatersData = dataHolder.getRepeater(ContentBackupAmetysObject.AUTOSAVE_REPEATERS); 232 for (ModelAwareRepeaterEntry entry : repeatersData.getEntries()) 233 { 234 Map<String, Object> repeater = new HashMap<>(); 235 236 repeater.put("name", entry.getValue(ContentBackupAmetysObject.REPEATER_NAME, false, StringUtils.EMPTY)); 237 repeater.put("prefix", entry.getValue(ContentBackupAmetysObject.REPEATER_PREFIX, false, StringUtils.EMPTY)); 238 repeater.put("count", entry.getValue(ContentBackupAmetysObject.REPEATER_COUNT, false, "0")); 239 240 repeaters.add(repeater); 241 } 242 243 return repeaters; 244 } 245 246 /** 247 * Delete an automatic backup for a content. 248 * @param contentId The content id 249 * @return A empty map 250 */ 251 @Callable 252 public Map<String, Object> deleteContentBackup (String contentId) 253 { 254 if (StringUtils.isNotEmpty(contentId)) 255 { 256 if (getLogger().isDebugEnabled()) 257 { 258 getLogger().debug(String.format("Delete automatic backup for content '%s'", contentId)); 259 } 260 261 RemovableAmetysObject contentNode = getContentNode(contentId, false); 262 263 if (contentNode != null) 264 { 265 ModifiableAmetysObject parent = (ModifiableAmetysObject) contentNode.getParent(); 266 267 contentNode.remove(); 268 269 parent.saveChanges(); 270 } 271 } 272 273 return java.util.Collections.EMPTY_MAP; 274 } 275 276 /** 277 * Store an automatic backup for a content. 278 * @param contentId The content id 279 * @param values The valid values to store 280 * @param invalidValues The invalid values to store 281 * @param comments The comments as JSON string 282 * @param repeaters The repeaters to store 283 * @return A empty Map 284 */ 285 @Callable 286 public Map<String, Object> setContentBackup (String contentId, Map<String, Object> values, Map<String, Object> invalidValues, String comments, Collection<Map<String, String>> repeaters) 287 { 288 if (getLogger().isDebugEnabled()) 289 { 290 getLogger().debug(String.format("Start automatic backup for content '%s'", contentId)); 291 } 292 293 UserIdentity currentUser = _currentUserProvider.getUser(); 294 295 ModelAwareJCRAmetysObject contentNode = getContentNode(contentId, true); 296 297 // Remove all existing metadata. 298 removeAllRepeaterEntries(contentNode); 299 300 contentNode.setValue(ContentBackupAmetysObject.AUTOSAVE_TEMP_DATE, ZonedDateTime.now()); 301 contentNode.setValue(ContentBackupAmetysObject.AUTOSAVE_CREATOR, currentUser); 302 303 // Store the valid values. 304 storeValues(contentNode, ContentBackupAmetysObject.AUTOSAVE_VALUES, values); 305 306 // Store the invalid values. 307 storeValues(contentNode, ContentBackupAmetysObject.AUTOSAVE_INVALID_VALUES, invalidValues); 308 309 // Store the comments. 310 contentNode.setValue(ContentBackupAmetysObject.AUTOSAVE_COMMENTS, comments); 311 312 // Store the repeater item counts. 313 storeRepeaters(contentNode, repeaters); 314 315 contentNode.saveChanges(); 316 317 return java.util.Collections.EMPTY_MAP; 318 } 319 320 /** 321 * Store the attribute values 322 * @param dataHolder the content backup data holder 323 * @param valuesRepeaterPath the path of the values attribute 324 * @param values the values to store, as a Map of values, indexed by name. 325 */ 326 protected void storeValues(ModifiableModelAwareDataHolder dataHolder, String valuesRepeaterPath, Map<String, Object> values) 327 { 328 ModifiableModelAwareRepeater valuesData = dataHolder.getRepeater(valuesRepeaterPath, true); 329 for (String name : values.keySet()) 330 { 331 ModifiableModelAwareRepeaterEntry entry = valuesData.addEntry(); 332 333 entry.setValue(ContentBackupAmetysObject.ATTRIBUTE_NAME, name); 334 335 // Store value as JSON string 336 Object value = values.get(name); 337 entry.setValue(ContentBackupAmetysObject.ATTRIBUTE_VALUE, _encodeValue(value)); 338 } 339 } 340 341 private String _encodeValue (Object value) 342 { 343 if (value instanceof Map || value instanceof List) 344 { 345 return _jsonUtils.convertObjectToJson(value); 346 } 347 else 348 { 349 return value != null ? value.toString() : ""; 350 } 351 } 352 353 /** 354 * Store the repeater item counts to be able to re-initialize them 355 * @param dataHolder the content backup data holder 356 * @param repeaters the repeaters data 357 */ 358 protected void storeRepeaters(ModifiableModelAwareDataHolder dataHolder, Collection<Map<String, String>> repeaters) 359 { 360 ModifiableModelAwareRepeater repeatersData = dataHolder.getRepeater(ContentBackupAmetysObject.AUTOSAVE_REPEATERS, true); 361 for (Map<String, String> repeaterData : repeaters) 362 { 363 ModifiableModelAwareRepeaterEntry entry = repeatersData.addEntry(); 364 entry.setValue(ContentBackupAmetysObject.REPEATER_NAME, repeaterData.get("name")); 365 entry.setValue(ContentBackupAmetysObject.REPEATER_PREFIX, repeaterData.get("prefix")); 366 entry.setValue(ContentBackupAmetysObject.REPEATER_COUNT, repeaterData.get("count")); 367 } 368 } 369 370 /** 371 * Remove all the repeater entries of a given data holder. 372 * @param dataHolder the data holder to clear. 373 */ 374 protected void removeAllRepeaterEntries(ModifiableModelAwareDataHolder dataHolder) 375 { 376 for (String dataName : dataHolder.getDataNames()) 377 { 378 if (org.ametys.plugins.repository.data.type.ModelItemTypeConstants.REPEATER_TYPE_ID.equals(dataHolder.getType(dataName).getId())) 379 { 380 dataHolder.removeValue(dataName); 381 } 382 } 383 } 384 385 /** 386 * Get the storage node for a content in the automatic backup space, or create it if it doesn't exist. 387 * @param contentId the content ID. 388 * @param createNew <code>true</code> to create automatically the node when missing. 389 * @return the content backup storage node. 390 */ 391 protected ModelAwareJCRAmetysObject getContentNode(String contentId, boolean createNew) 392 { 393 ModelAwareJCRAmetysObject contentNode = null; 394 395 // Query the content node by path and contentId property. 396 Expression expression = new StringExpression(ContentBackupAmetysObject.AUTOSAVE_CONTENT_ID, Operator.EQ, contentId); 397 String query = QueryHelper.getXPathQuery(null, __AUTOSAVE_NODETYPE, expression); 398 399 AmetysObjectIterable<ModelAwareJCRAmetysObject> autoSaveObjects = _resolver.query(query); 400 Iterator<ModelAwareJCRAmetysObject> it = autoSaveObjects.iterator(); 401 402 // Get or create the content node. 403 if (it.hasNext()) 404 { 405 contentNode = it.next(); 406 } 407 else if (createNew) 408 { 409 ModifiableTraversableAmetysObject tempRoot = getOrCreateTempRoot(); 410 411 String objectName = FilterNameHelper.filterName(contentId); 412 contentNode = (ModelAwareJCRAmetysObject) tempRoot.createChild(objectName, __AUTOSAVE_NODETYPE); 413 contentNode.setValue(ContentBackupAmetysObject.AUTOSAVE_CONTENT_ID, contentId); 414 415 tempRoot.saveChanges(); 416 } 417 418 return contentNode; 419 } 420 421 /** 422 * Get the temporary backup root, or create it if it doesn't exist. 423 * @return the temporary backup root. 424 */ 425 protected ModifiableTraversableAmetysObject getOrCreateTempRoot() 426 { 427 ModifiableTraversableAmetysObject pluginsRoot = _resolver.resolveByPath("/ametys:plugins"); 428 429 ModifiableTraversableAmetysObject cmsNode = null; 430 if (pluginsRoot.hasChild("cms")) 431 { 432 cmsNode = (ModifiableTraversableAmetysObject) pluginsRoot.getChild("cms"); 433 } 434 else 435 { 436 cmsNode = (ModifiableTraversableAmetysObject) pluginsRoot.createChild("cms", "ametys:unstructured"); 437 } 438 439 ModifiableTraversableAmetysObject editionNode = null; 440 if (cmsNode.hasChild("edition")) 441 { 442 editionNode = (ModifiableTraversableAmetysObject) cmsNode.getChild("edition"); 443 } 444 else 445 { 446 editionNode = (ModifiableTraversableAmetysObject) cmsNode.createChild("edition", "ametys:unstructured"); 447 } 448 449 ModifiableTraversableAmetysObject tempNode = null; 450 if (editionNode.hasChild("temp")) 451 { 452 tempNode = (ModifiableTraversableAmetysObject) editionNode.getChild("temp"); 453 } 454 else 455 { 456 tempNode = (ModifiableTraversableAmetysObject) editionNode.createChild("temp", "ametys:unstructured"); 457 } 458 459 if (pluginsRoot.needsSave()) 460 { 461 pluginsRoot.saveChanges(); 462 } 463 464 return tempNode; 465 } 466}