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.util.ArrayList; 019import java.util.Collection; 020import java.util.Date; 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.lang.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.RepositoryConstants; 043import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject; 044import org.ametys.plugins.repository.metadata.CompositeMetadata; 045import org.ametys.plugins.repository.metadata.CompositeMetadata.MetadataType; 046import org.ametys.plugins.repository.metadata.MetadataAwareAmetysObject; 047import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; 048import org.ametys.plugins.repository.metadata.UnknownMetadataException; 049import org.ametys.plugins.repository.query.QueryHelper; 050import org.ametys.plugins.repository.query.expression.Expression; 051import org.ametys.plugins.repository.query.expression.Expression.Operator; 052import org.ametys.plugins.repository.query.expression.StringExpression; 053import org.ametys.runtime.config.Config; 054 055/** 056 * Component for manipulating auto-backup on contents 057 * 058 */ 059public class ContentBackupClientInteraction extends AbstractLogEnabled implements Serviceable, Component 060{ 061 private static final String __AUTOSAVE_NODETYPE = RepositoryConstants.NAMESPACE_PREFIX + ":autoSave"; 062 063 private static final String __AUTOSAVE_CREATOR = "creator"; 064 private static final String __AUTOSAVE_TEMP_DATE = "tempContentDate"; 065 private static final String __AUTOSAVE_COMMENTS = "jsonComments"; 066 private static final String __AUTOSAVE_VALUES = "values"; 067 private static final String __AUTOSAVE_INVALID_VALUES = "invalid"; 068 private static final String __AUTOSAVE_REPEATERS = "repeaters"; 069 070 private static final String __METADATA_NAME = "name"; 071 private static final String __METADATA_VALUE = "value"; 072 073 private static final String __REPEATER_NAME = "name"; 074 private static final String __REPEATER_COUNT = "count"; 075 private static final String __REPEATER_PREFIX = "prefix"; 076 077 private AmetysObjectResolver _resolver; 078 private CurrentUserProvider _currentUserProvider; 079 private JSONUtils _jsonUtils; 080 081 @Override 082 public void service(ServiceManager manager) throws ServiceException 083 { 084 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 085 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 086 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 087 } 088 089 /** 090 * Get the content backup information 091 * @param contentId The content id 092 * @return The saved data 093 */ 094 @Callable 095 public Map<String, Object> getContentBackup (String contentId) 096 { 097 if (getLogger().isDebugEnabled()) 098 { 099 getLogger().debug(String.format("Get automatic data backup for content '%s'", contentId)); 100 } 101 102 Map<String, Object> result = new HashMap<>(); 103 104 boolean autoSaveEnabled = Config.getInstance().getValue("automatic.save.enabled"); 105 Long autoSaveFrequency = Config.getInstance().getValue("automatic.save.frequency"); 106 107 result.put("enabled", autoSaveEnabled); 108 result.put("frequency", autoSaveFrequency); 109 110 MetadataAwareAmetysObject contentNode = getContentNode(contentId, false); 111 112 if (contentNode != null) 113 { 114 Map<String, Object> autoBackup = new HashMap<>(); 115 116 CompositeMetadata metadataHolder = contentNode.getMetadataHolder(); 117 118 autoBackup.put("creator", metadataHolder.getString(__AUTOSAVE_CREATOR, "")); 119 autoBackup.put("contentId", contentId); 120 121 Date backupDate = metadataHolder.getDate(__AUTOSAVE_TEMP_DATE, null); 122 if (backupDate != null) 123 { 124 autoBackup.put("date", DateUtils.dateToString(backupDate)); 125 } 126 127 autoBackup.put("data", _getBackupData(metadataHolder)); 128 129 result.put("auto-backup", autoBackup); 130 } 131 132 return result; 133 } 134 135 private Map<String, Object> _getBackupData (CompositeMetadata metadataHolder) 136 { 137 Map<String, Object> data = new HashMap<>(); 138 139 // Get and generate the repeater item counts. 140 try 141 { 142 List<Map<String, Object>> repeaters = new ArrayList<>(); 143 144 CompositeMetadata repeatersMeta = metadataHolder.getCompositeMetadata(__AUTOSAVE_REPEATERS); 145 for (String name : repeatersMeta.getMetadataNames()) 146 { 147 if (repeatersMeta.getType(name).equals(MetadataType.COMPOSITE)) 148 { 149 CompositeMetadata composite = repeatersMeta.getCompositeMetadata(name); 150 151 Map<String, Object> repeater = new HashMap<>(); 152 153 repeater.put("name", composite.getString(__REPEATER_NAME, "")); 154 repeater.put("prefix", composite.getString(__REPEATER_PREFIX, "")); 155 repeater.put("count", composite.getString(__REPEATER_COUNT, "0")); 156 157 repeaters.add(repeater); 158 } 159 } 160 161 data.put("repeaters", repeaters); 162 } 163 catch (UnknownMetadataException e) 164 { 165 // Ignore 166 } 167 168 // Get and generate the valid metadata values. 169 try 170 { 171 List<Map<String, Object>> metadatas = new ArrayList<>(); 172 173 CompositeMetadata values = metadataHolder.getCompositeMetadata(__AUTOSAVE_VALUES); 174 for (String name : values.getMetadataNames()) 175 { 176 if (values.getType(name).equals(MetadataType.COMPOSITE)) 177 { 178 Map<String, Object> metadata = new HashMap<>(); 179 180 CompositeMetadata composite = values.getCompositeMetadata(name); 181 182 metadata.put("name", composite.getString(__METADATA_NAME, "")); 183 184 String encodedValue = composite.getString(__METADATA_VALUE, ""); 185 metadata.put("value", _decodeValue(encodedValue)); 186 metadatas.add(metadata); 187 } 188 } 189 190 data.put("metadatas", metadatas); 191 } 192 catch (UnknownMetadataException e) 193 { 194 // Ignore 195 } 196 197 // Get and generate the invalid metadata values. 198 try 199 { 200 List<Map<String, Object>> invalidMetadatas = new ArrayList<>(); 201 202 CompositeMetadata invalidValues = metadataHolder.getCompositeMetadata(__AUTOSAVE_INVALID_VALUES); 203 for (String name : invalidValues.getMetadataNames()) 204 { 205 if (invalidValues.getType(name).equals(MetadataType.COMPOSITE)) 206 { 207 Map<String, Object> invalidMetadata = new HashMap<>(); 208 209 CompositeMetadata composite = invalidValues.getCompositeMetadata(name); 210 211 invalidMetadata.put("name", composite.getString(__METADATA_NAME, "")); 212 String encodedValue = composite.getString(__METADATA_VALUE, ""); 213 invalidMetadata.put("value", _decodeValue(encodedValue)); 214 215 invalidMetadatas.add(invalidMetadata); 216 } 217 } 218 219 data.put("invalid-metadatas", invalidMetadatas); 220 } 221 catch (UnknownMetadataException e) 222 { 223 // Ignore 224 } 225 226 // Get the field comments 227 data.put("comments", metadataHolder.getString(__AUTOSAVE_COMMENTS, "{}")); 228 229 return data; 230 } 231 232 /** 233 * Delete an automatic backup for a content. 234 * @param contentId The content id 235 * @return A empty map 236 */ 237 @Callable 238 public Map<String, Object> deleteContentBackup (String contentId) 239 { 240 if (StringUtils.isNotEmpty(contentId)) 241 { 242 if (getLogger().isDebugEnabled()) 243 { 244 getLogger().debug(String.format("Delete automatic backup for content '%s'", contentId)); 245 } 246 247 // Query the content node by path and contentId property. 248 Expression expression = new StringExpression("contentId", Operator.EQ, contentId); 249 String query = QueryHelper.getXPathQuery(null, __AUTOSAVE_NODETYPE, expression); 250 251 AmetysObjectIterable<DefaultTraversableAmetysObject> contentObjects = _resolver.query(query); 252 Iterator<DefaultTraversableAmetysObject> it = contentObjects.iterator(); 253 254 // Get or create the content node. 255 if (it.hasNext()) 256 { 257 DefaultTraversableAmetysObject object = it.next(); 258 259 ModifiableAmetysObject parent = (ModifiableAmetysObject) object.getParent(); 260 261 object.remove(); 262 263 parent.saveChanges(); 264 } 265 } 266 267 return java.util.Collections.EMPTY_MAP; 268 } 269 270 /** 271 * Store an automatic backup for a content. 272 * @param contentId The content id 273 * @param values The valid values to store 274 * @param invalidValues The invalid values to store 275 * @param comments The comments as JSON string 276 * @param repeaters The repeaters to store 277 * @return A empty Map 278 */ 279 @Callable 280 public Map<String, Object> setContentBackup (String contentId, Map<String, Object> values, Map<String, Object> invalidValues, String comments, Collection<Map<String, String>> repeaters) 281 { 282 if (getLogger().isDebugEnabled()) 283 { 284 getLogger().debug(String.format("Start automatic backup for content '%s'", contentId)); 285 } 286 287 UserIdentity currentUser = _currentUserProvider.getUser(); 288 289 DefaultTraversableAmetysObject contentNode = getContentNode(contentId, true); 290 ModifiableCompositeMetadata meta = contentNode.getMetadataHolder(); 291 292 // Remove all existing metadata. 293 removeAllComposites(meta); 294 295 meta.setMetadata(__AUTOSAVE_TEMP_DATE, new Date()); 296 297 ModifiableCompositeMetadata creatorMetadata = meta.getCompositeMetadata(__AUTOSAVE_CREATOR, true); 298 creatorMetadata.setMetadata("login", currentUser.getLogin()); 299 creatorMetadata.setMetadata("populationId", currentUser.getPopulationId()); 300 301 // Get the four sub-parts: valid values, invalid values, comments and repeater item counts. 302 ModifiableCompositeMetadata valuesMeta = meta.getCompositeMetadata(__AUTOSAVE_VALUES, true); 303 ModifiableCompositeMetadata invalidValuesMeta = meta.getCompositeMetadata(__AUTOSAVE_INVALID_VALUES, true); 304 ModifiableCompositeMetadata repeatersMeta = meta.getCompositeMetadata(__AUTOSAVE_REPEATERS, true); 305 306 // Store the valid values. 307 storeValues(values, valuesMeta); 308 309 // Store the invalid values. 310 storeValues(invalidValues, invalidValuesMeta); 311 312 // Store the comments. 313 meta.setMetadata(__AUTOSAVE_COMMENTS, comments); 314 315 // Store the repeater item counts. 316 storeRepeaters(repeaters, repeatersMeta); 317 318 contentNode.saveChanges(); 319 320 return java.util.Collections.EMPTY_MAP; 321 } 322 323 /** 324 * Store the metadata values (in composites named 1, 2, 3...) 325 * @param values the meta values to store, as a Map of values, indexed by name. 326 * @param valuesMeta the composite metadata to store the values in. 327 */ 328 protected void storeValues(Map<String, Object> values, ModifiableCompositeMetadata valuesMeta) 329 { 330 int metaIndex = 1; 331 for (String name : values.keySet()) 332 { 333 ModifiableCompositeMetadata composite = valuesMeta.getCompositeMetadata(Integer.toString(metaIndex), true); 334 335 Object value = values.get(name); 336 337 composite.setMetadata(__METADATA_NAME, name); 338 // Store value as JSON string 339 composite.setMetadata(__METADATA_VALUE, _encodeValue(value)); 340 341 metaIndex++; 342 } 343 } 344 345 private String _encodeValue (Object value) 346 { 347 if (value instanceof Map || value instanceof List) 348 { 349 return _jsonUtils.convertObjectToJson(value); 350 } 351 else 352 { 353 return value != null ? value.toString() : ""; 354 } 355 } 356 357 private Object _decodeValue (String value) 358 { 359 if (StringUtils.isBlank(value)) 360 { 361 return ""; 362 } 363 364 try 365 { 366 return _jsonUtils.convertJsonToMap(value); 367 } 368 catch (IllegalArgumentException e) 369 { 370 // Failed to convert into map, continue 371 } 372 373 try 374 { 375 return _jsonUtils.convertJsonToList(value); 376 } 377 catch (IllegalArgumentException e) 378 { 379 // Failed to convert into list, continue 380 } 381 382 try 383 { 384 return _jsonUtils.convertJsonToArray(value); 385 } 386 catch (IllegalArgumentException e) 387 { 388 // Failed to convert into array, continue 389 } 390 391 return value; 392 } 393 394 /** 395 * Store the repeater item counts to be able to re-initialize them. 396 * @param repeaters the repeaters data. 397 * @param repeatersMeta the composite metadata to store the repeaters in. 398 */ 399 protected void storeRepeaters(Collection<Map<String, String>> repeaters, ModifiableCompositeMetadata repeatersMeta) 400 { 401 int repeaterIndex = 1; 402 for (Map<String, String> repeaterData : repeaters) 403 { 404 ModifiableCompositeMetadata composite = repeatersMeta.getCompositeMetadata(Integer.toString(repeaterIndex), true); 405 composite.setMetadata(__REPEATER_NAME, repeaterData.get("name")); 406 composite.setMetadata(__REPEATER_PREFIX, repeaterData.get("prefix")); 407 composite.setMetadata(__REPEATER_COUNT, repeaterData.get("count")); 408 409 repeaterIndex++; 410 } 411 } 412 413 /** 414 * Remove all the composite metadatas of a given composite. 415 * @param meta the metadata to clear. 416 */ 417 protected void removeAllComposites(ModifiableCompositeMetadata meta) 418 { 419 for (String metaName : meta.getMetadataNames()) 420 { 421 if (meta.getType(metaName).equals(MetadataType.COMPOSITE)) 422 { 423 meta.removeMetadata(metaName); 424 } 425 } 426 } 427 428 /** 429 * Get the storage node for a content in the automatic backup space, or create it if it doesn't exist. 430 * @param contentId the content ID. 431 * @param createNew <code>true</code> to create automatically the node when missing. 432 * @return the content backup storage node. 433 */ 434 protected DefaultTraversableAmetysObject getContentNode(String contentId, boolean createNew) 435 { 436 DefaultTraversableAmetysObject contentNode = null; 437 438 // Query the content node by path and contentId property. 439 Expression expression = new StringExpression("contentId", Operator.EQ, contentId); 440 String query = QueryHelper.getXPathQuery(null, __AUTOSAVE_NODETYPE, expression); 441 442 AmetysObjectIterable<DefaultTraversableAmetysObject> autoSaveObjects = _resolver.query(query); 443 Iterator<DefaultTraversableAmetysObject> it = autoSaveObjects.iterator(); 444 445 // Get or create the content node. 446 if (it.hasNext()) 447 { 448 contentNode = it.next(); 449 } 450 else if (createNew) 451 { 452 DefaultTraversableAmetysObject tempRoot = getOrCreateTempRoot(); 453 454 String objectName = FilterNameHelper.filterName(contentId); 455 contentNode = (DefaultTraversableAmetysObject) tempRoot.createChild(objectName, __AUTOSAVE_NODETYPE); 456 contentNode.getMetadataHolder().setMetadata("contentId", contentId); 457 458 tempRoot.saveChanges(); 459 } 460 461 return contentNode; 462 } 463 464 /** 465 * Get the temporary backup root, or create it if it doesn't exist. 466 * @return the temporary backup root. 467 */ 468 protected DefaultTraversableAmetysObject getOrCreateTempRoot() 469 { 470 DefaultTraversableAmetysObject pluginsRoot = _resolver.resolveByPath("/ametys:plugins"); 471 472 DefaultTraversableAmetysObject cmsNode = null; 473 if (pluginsRoot.hasChild("cms")) 474 { 475 cmsNode = (DefaultTraversableAmetysObject) pluginsRoot.getChild("cms"); 476 } 477 else 478 { 479 cmsNode = (DefaultTraversableAmetysObject) pluginsRoot.createChild("cms", "ametys:unstructured"); 480 } 481 482 DefaultTraversableAmetysObject editionNode = null; 483 if (cmsNode.hasChild("edition")) 484 { 485 editionNode = (DefaultTraversableAmetysObject) cmsNode.getChild("edition"); 486 } 487 else 488 { 489 editionNode = (DefaultTraversableAmetysObject) cmsNode.createChild("edition", "ametys:unstructured"); 490 } 491 492 DefaultTraversableAmetysObject tempNode = null; 493 if (editionNode.hasChild("temp")) 494 { 495 tempNode = (DefaultTraversableAmetysObject) editionNode.getChild("temp"); 496 } 497 else 498 { 499 tempNode = (DefaultTraversableAmetysObject) editionNode.createChild("temp", "ametys:unstructured"); 500 } 501 502 if (pluginsRoot.needsSave()) 503 { 504 pluginsRoot.saveChanges(); 505 } 506 507 return tempNode; 508 } 509}