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.core.ui.Callable; 034import org.ametys.core.user.CurrentUserProvider; 035import org.ametys.core.user.UserIdentity; 036import org.ametys.core.util.DateUtils; 037import org.ametys.core.util.JSONUtils; 038import org.ametys.plugins.repository.AmetysObjectIterable; 039import org.ametys.plugins.repository.AmetysObjectResolver; 040import org.ametys.plugins.repository.ModifiableAmetysObject; 041import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 042import org.ametys.plugins.repository.RemovableAmetysObject; 043import org.ametys.plugins.repository.RepositoryConstants; 044import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 045import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder; 046import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater; 047import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry; 048import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareRepeater; 049import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareRepeaterEntry; 050import org.ametys.plugins.repository.jcr.ModelAwareJCRAmetysObject; 051import org.ametys.plugins.repository.jcr.NameHelper; 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 and generate the repeater item counts. 147 if (dataHolder.hasValue(ContentBackupAmetysObject.AUTOSAVE_REPEATERS)) 148 { 149 List<Map<String, Object>> repeaters = getRepeatersBackupData(dataHolder); 150 data.put("repeaters", repeaters); 151 } 152 153 return data; 154 } 155 156 /** 157 * Retrieves the values backup information 158 * @param dataHolder the content backup data holder 159 * @param valuesRepeaterPath the path of the values attribute 160 * @return the values backup information 161 */ 162 protected List<Map<String, Object>> getValuesBackupData(ModelAwareDataHolder dataHolder, String valuesRepeaterPath) 163 { 164 List<Map<String, Object>> attributes = new ArrayList<>(); 165 166 ModelAwareRepeater values = dataHolder.getRepeater(valuesRepeaterPath); 167 for (ModelAwareRepeaterEntry entry : values.getEntries()) 168 { 169 Map<String, Object> attribute = new HashMap<>(); 170 171 attribute.put("name", entry.getValue(ContentBackupAmetysObject.ATTRIBUTE_NAME, false, StringUtils.EMPTY)); 172 173 String encodedValue = entry.getValue(ContentBackupAmetysObject.ATTRIBUTE_VALUE, false, StringUtils.EMPTY); 174 attribute.put("value", _decodeValue(encodedValue)); 175 176 attributes.add(attribute); 177 } 178 179 return attributes; 180 } 181 182 private Object _decodeValue (String value) 183 { 184 if (StringUtils.isBlank(value)) 185 { 186 return StringUtils.EMPTY; 187 } 188 189 try 190 { 191 return _jsonUtils.convertJsonToMap(value); 192 } 193 catch (IllegalArgumentException e) 194 { 195 // Failed to convert into map, continue 196 } 197 198 try 199 { 200 return _jsonUtils.convertJsonToList(value); 201 } 202 catch (IllegalArgumentException e) 203 { 204 // Failed to convert into list, continue 205 } 206 207 try 208 { 209 return _jsonUtils.convertJsonToArray(value); 210 } 211 catch (IllegalArgumentException e) 212 { 213 // Failed to convert into array, continue 214 } 215 216 return value; 217 } 218 219 /** 220 * Retrieves the repeaters backup information 221 * @param dataHolder the content backup data holder 222 * @return the repeaters backup information 223 */ 224 protected List<Map<String, Object>> getRepeatersBackupData(ModelAwareDataHolder dataHolder) 225 { 226 List<Map<String, Object>> repeaters = new ArrayList<>(); 227 228 ModelAwareRepeater repeatersData = dataHolder.getRepeater(ContentBackupAmetysObject.AUTOSAVE_REPEATERS); 229 for (ModelAwareRepeaterEntry entry : repeatersData.getEntries()) 230 { 231 Map<String, Object> repeater = new HashMap<>(); 232 233 repeater.put("name", entry.getValue(ContentBackupAmetysObject.REPEATER_NAME, false, StringUtils.EMPTY)); 234 repeater.put("prefix", entry.getValue(ContentBackupAmetysObject.REPEATER_PREFIX, false, StringUtils.EMPTY)); 235 repeater.put("count", entry.getValue(ContentBackupAmetysObject.REPEATER_COUNT, false, "0")); 236 237 repeaters.add(repeater); 238 } 239 240 return repeaters; 241 } 242 243 /** 244 * Delete an automatic backup for a content. 245 * @param contentId The content id 246 * @return A empty map 247 */ 248 @Callable 249 public Map<String, Object> deleteContentBackup (String contentId) 250 { 251 if (StringUtils.isNotEmpty(contentId)) 252 { 253 if (getLogger().isDebugEnabled()) 254 { 255 getLogger().debug(String.format("Delete automatic backup for content '%s'", contentId)); 256 } 257 258 RemovableAmetysObject contentNode = getContentNode(contentId, false); 259 260 if (contentNode != null) 261 { 262 ModifiableAmetysObject parent = (ModifiableAmetysObject) contentNode.getParent(); 263 264 contentNode.remove(); 265 266 parent.saveChanges(); 267 } 268 } 269 270 return java.util.Collections.EMPTY_MAP; 271 } 272 273 /** 274 * Store an automatic backup for a content. 275 * @param contentId The content id 276 * @param values The valid values to store 277 * @param invalidValues The invalid values to store 278 * @param repeaters The repeaters to store 279 * @return A empty Map 280 */ 281 @Callable 282 public Map<String, Object> setContentBackup (String contentId, Map<String, Object> values, Map<String, Object> invalidValues, Collection<Map<String, String>> repeaters) 283 { 284 if (getLogger().isDebugEnabled()) 285 { 286 getLogger().debug(String.format("Start automatic backup for content '%s'", contentId)); 287 } 288 289 UserIdentity currentUser = _currentUserProvider.getUser(); 290 291 ModelAwareJCRAmetysObject contentNode = getContentNode(contentId, true); 292 293 // Remove all existing metadata. 294 removeAllRepeaterEntries(contentNode); 295 296 contentNode.setValue(ContentBackupAmetysObject.AUTOSAVE_TEMP_DATE, ZonedDateTime.now()); 297 contentNode.setValue(ContentBackupAmetysObject.AUTOSAVE_CREATOR, currentUser); 298 299 // Store the valid values. 300 storeValues(contentNode, ContentBackupAmetysObject.AUTOSAVE_VALUES, values); 301 302 // Store the invalid values. 303 storeValues(contentNode, ContentBackupAmetysObject.AUTOSAVE_INVALID_VALUES, invalidValues); 304 305 // Store the repeater item counts. 306 storeRepeaters(contentNode, repeaters); 307 308 contentNode.saveChanges(); 309 310 return java.util.Collections.EMPTY_MAP; 311 } 312 313 /** 314 * Store the attribute values 315 * @param dataHolder the content backup data holder 316 * @param valuesRepeaterPath the path of the values attribute 317 * @param values the values to store, as a Map of values, indexed by name. 318 */ 319 protected void storeValues(ModifiableModelAwareDataHolder dataHolder, String valuesRepeaterPath, Map<String, Object> values) 320 { 321 ModifiableModelAwareRepeater valuesData = dataHolder.getRepeater(valuesRepeaterPath, true); 322 for (String name : values.keySet()) 323 { 324 ModifiableModelAwareRepeaterEntry entry = valuesData.addEntry(); 325 326 entry.setValue(ContentBackupAmetysObject.ATTRIBUTE_NAME, name); 327 328 // Store value as JSON string 329 Object value = values.get(name); 330 entry.setValue(ContentBackupAmetysObject.ATTRIBUTE_VALUE, _encodeValue(value)); 331 } 332 } 333 334 private String _encodeValue (Object value) 335 { 336 if (value instanceof Map || value instanceof List) 337 { 338 return _jsonUtils.convertObjectToJson(value); 339 } 340 else 341 { 342 return value != null ? value.toString() : ""; 343 } 344 } 345 346 /** 347 * Store the repeater item counts to be able to re-initialize them 348 * @param dataHolder the content backup data holder 349 * @param repeaters the repeaters data 350 */ 351 protected void storeRepeaters(ModifiableModelAwareDataHolder dataHolder, Collection<Map<String, String>> repeaters) 352 { 353 ModifiableModelAwareRepeater repeatersData = dataHolder.getRepeater(ContentBackupAmetysObject.AUTOSAVE_REPEATERS, true); 354 for (Map<String, String> repeaterData : repeaters) 355 { 356 ModifiableModelAwareRepeaterEntry entry = repeatersData.addEntry(); 357 entry.setValue(ContentBackupAmetysObject.REPEATER_NAME, repeaterData.get("name")); 358 entry.setValue(ContentBackupAmetysObject.REPEATER_PREFIX, repeaterData.get("prefix")); 359 entry.setValue(ContentBackupAmetysObject.REPEATER_COUNT, repeaterData.get("count")); 360 } 361 } 362 363 /** 364 * Remove all the repeater entries of a given data holder. 365 * @param dataHolder the data holder to clear. 366 */ 367 protected void removeAllRepeaterEntries(ModifiableModelAwareDataHolder dataHolder) 368 { 369 for (String dataName : dataHolder.getDataNames()) 370 { 371 if (org.ametys.plugins.repository.data.type.ModelItemTypeConstants.REPEATER_TYPE_ID.equals(dataHolder.getType(dataName).getId())) 372 { 373 dataHolder.removeValue(dataName); 374 } 375 } 376 } 377 378 /** 379 * Get the storage node for a content in the automatic backup space, or create it if it doesn't exist. 380 * @param contentId the content ID. 381 * @param createNew <code>true</code> to create automatically the node when missing. 382 * @return the content backup storage node. 383 */ 384 protected ModelAwareJCRAmetysObject getContentNode(String contentId, boolean createNew) 385 { 386 ModelAwareJCRAmetysObject contentNode = null; 387 388 // Query the content node by path and contentId property. 389 Expression expression = new StringExpression(ContentBackupAmetysObject.AUTOSAVE_CONTENT_ID, Operator.EQ, contentId); 390 String query = QueryHelper.getXPathQuery(null, __AUTOSAVE_NODETYPE, expression); 391 392 AmetysObjectIterable<ModelAwareJCRAmetysObject> autoSaveObjects = _resolver.query(query); 393 Iterator<ModelAwareJCRAmetysObject> it = autoSaveObjects.iterator(); 394 395 // Get or create the content node. 396 if (it.hasNext()) 397 { 398 contentNode = it.next(); 399 } 400 else if (createNew) 401 { 402 ModifiableTraversableAmetysObject tempRoot = getOrCreateTempRoot(); 403 404 String objectName = NameHelper.filterName(contentId); 405 contentNode = (ModelAwareJCRAmetysObject) tempRoot.createChild(objectName, __AUTOSAVE_NODETYPE); 406 contentNode.setValue(ContentBackupAmetysObject.AUTOSAVE_CONTENT_ID, contentId); 407 408 tempRoot.saveChanges(); 409 } 410 411 return contentNode; 412 } 413 414 /** 415 * Get the temporary backup root, or create it if it doesn't exist. 416 * @return the temporary backup root. 417 */ 418 protected ModifiableTraversableAmetysObject getOrCreateTempRoot() 419 { 420 ModifiableTraversableAmetysObject pluginsRoot = _resolver.resolveByPath("/ametys:plugins"); 421 422 ModifiableTraversableAmetysObject cmsNode = null; 423 if (pluginsRoot.hasChild("cms")) 424 { 425 cmsNode = (ModifiableTraversableAmetysObject) pluginsRoot.getChild("cms"); 426 } 427 else 428 { 429 cmsNode = (ModifiableTraversableAmetysObject) pluginsRoot.createChild("cms", "ametys:unstructured"); 430 } 431 432 ModifiableTraversableAmetysObject editionNode = null; 433 if (cmsNode.hasChild("edition")) 434 { 435 editionNode = (ModifiableTraversableAmetysObject) cmsNode.getChild("edition"); 436 } 437 else 438 { 439 editionNode = (ModifiableTraversableAmetysObject) cmsNode.createChild("edition", "ametys:unstructured"); 440 } 441 442 ModifiableTraversableAmetysObject tempNode = null; 443 if (editionNode.hasChild("temp")) 444 { 445 tempNode = (ModifiableTraversableAmetysObject) editionNode.getChild("temp"); 446 } 447 else 448 { 449 tempNode = (ModifiableTraversableAmetysObject) editionNode.createChild("temp", "ametys:unstructured"); 450 } 451 452 if (pluginsRoot.needsSave()) 453 { 454 pluginsRoot.saveChanges(); 455 } 456 457 return tempNode; 458 } 459}