001/* 002 * (C) Copyright 2018-2020 Nuxeo (http://nuxeo.com/) and others. 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 * 016 * Contributors: 017 * Funsho David 018 */ 019 020package org.nuxeo.ecm.platform.comment.impl; 021 022import static java.lang.Boolean.TRUE; 023import static org.nuxeo.ecm.core.api.security.ACL.LOCAL_ACL; 024import static org.nuxeo.ecm.core.query.sql.NXQL.ECM_NAME; 025import static org.nuxeo.ecm.core.query.sql.NXQL.ECM_PARENTID; 026import static org.nuxeo.ecm.core.query.sql.NXQL.ECM_PRIMARYTYPE; 027import static org.nuxeo.ecm.core.query.sql.NXQL.ECM_UUID; 028import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_PARENT_ID_PROPERTY; 029import static org.nuxeo.ecm.platform.comment.api.CommentConstants.MIGRATION_STATE_PROPERTY; 030import static org.nuxeo.ecm.platform.comment.api.CommentConstants.MIGRATION_STATE_RELATION; 031import static org.nuxeo.ecm.platform.comment.api.CommentConstants.MIGRATION_STATE_SECURED; 032import static org.nuxeo.ecm.platform.comment.api.CommentConstants.MIGRATION_STEP_PROPERTY_TO_SECURED; 033import static org.nuxeo.ecm.platform.comment.api.CommentConstants.MIGRATION_STEP_RELATION_TO_PROPERTY; 034import static org.nuxeo.ecm.platform.comment.impl.AbstractCommentManager.COMMENTS_DIRECTORY; 035import static org.nuxeo.ecm.platform.comment.impl.PropertyCommentManager.HIDDEN_FOLDER_TYPE; 036import static org.nuxeo.ecm.platform.ec.notification.NotificationConstants.DISABLE_NOTIFICATION_SERVICE; 037 038import java.util.ArrayList; 039import java.util.Collections; 040import java.util.List; 041import java.util.Map; 042import java.util.Set; 043import java.util.stream.Collectors; 044 045import org.apache.commons.lang3.StringUtils; 046import org.apache.logging.log4j.LogManager; 047import org.apache.logging.log4j.Logger; 048import org.nuxeo.ecm.core.api.CoreSession; 049import org.nuxeo.ecm.core.api.DocumentModel; 050import org.nuxeo.ecm.core.api.DocumentRef; 051import org.nuxeo.ecm.core.api.IdRef; 052import org.nuxeo.ecm.core.api.NuxeoException; 053import org.nuxeo.ecm.core.api.security.ACP; 054import org.nuxeo.ecm.core.migrator.AbstractRepositoryMigrator; 055import org.nuxeo.ecm.core.repository.RepositoryService; 056import org.nuxeo.ecm.platform.comment.service.CommentService; 057import org.nuxeo.ecm.platform.comment.service.CommentServiceConfig; 058import org.nuxeo.ecm.platform.relations.api.Graph; 059import org.nuxeo.ecm.platform.relations.api.RelationManager; 060import org.nuxeo.ecm.platform.relations.api.ResourceAdapter; 061import org.nuxeo.ecm.platform.relations.api.Statement; 062import org.nuxeo.ecm.platform.relations.api.impl.QNameResourceImpl; 063import org.nuxeo.runtime.api.Framework; 064import org.nuxeo.runtime.migration.MigrationService.MigrationContext; 065 066/** 067 * Migrator of comments. 068 * 069 * @since 10.3 070 */ 071public class CommentsMigrator extends AbstractRepositoryMigrator { 072 073 private static final Logger log = LogManager.getLogger(CommentsMigrator.class); 074 075 protected static final int BATCH_SIZE = 50; 076 077 public static final String UNMIGRATED_COMMENTS_FOLDER_NAME = "UnMigratedComments"; 078 079 /** 080 * @since 11.1. 081 */ 082 public static final String GET_COMMENTS_FOLDERS_QUERY = String.format( 083 "SELECT %s FROM Document WHERE %s = '%s' AND %s ='%s'", ECM_UUID, ECM_NAME, COMMENTS_DIRECTORY, 084 ECM_PRIMARYTYPE, HIDDEN_FOLDER_TYPE); 085 086 @Override 087 protected String probeSession(CoreSession session) { 088 CommentService commentComponent = (CommentService) Framework.getRuntime().getComponent(CommentService.NAME); 089 CommentServiceConfig commentServiceConfig = commentComponent.getConfig(); 090 if (commentServiceConfig != null) { 091 Graph graph = Framework.getService(RelationManager.class).getGraph(commentServiceConfig.graphName, session); 092 if (!graph.getStatements().isEmpty()) { 093 return MIGRATION_STATE_RELATION; 094 } 095 } 096 // If not in relation, check if there are still comments under hidden Comments folder(s) 097 // Do a new query one to check the step and the second the make the migration (do all comments) 098 if (hasUnsecuredComments(session)) { 099 return MIGRATION_STATE_PROPERTY; 100 } 101 // Else, comments are already secured 102 return MIGRATION_STATE_SECURED; 103 } 104 105 @Override 106 public void run(String step, MigrationContext migrationContext) { 107 if (!Set.of(MIGRATION_STEP_RELATION_TO_PROPERTY, MIGRATION_STEP_PROPERTY_TO_SECURED).contains(step)) { 108 throw new NuxeoException("Unknown migration step: " + step); 109 } 110 // needed for reportProgress 111 this.migrationContext = migrationContext; 112 reportProgress("Initializing", 0, -1); // unknown 113 List<String> repositoryNames = Framework.getService(RepositoryService.class).getRepositoryNames(); 114 try { 115 repositoryNames.forEach(repoName -> migrateRepository(step, migrationContext, repoName)); 116 } catch (MigrationShutdownException e) { 117 log.trace("Migration is shutdown"); 118 } 119 } 120 121 @Override 122 protected void migrateSession(String step, MigrationContext migrationContext, CoreSession session) { 123 if (MIGRATION_STEP_RELATION_TO_PROPERTY.equals(step)) { 124 migrateSessionRelationToProperty(session, migrationContext); 125 } else if (MIGRATION_STEP_PROPERTY_TO_SECURED.equals(step)) { 126 migrateSessionPropertyToSecured(session, migrationContext); 127 } 128 } 129 130 /** 131 * @since 11.1 132 */ 133 protected void migrateSessionRelationToProperty(CoreSession session, MigrationContext migrationContext) { 134 CommentService commentComponent = Framework.getService(CommentService.class); 135 CommentServiceConfig commentServiceConfig = commentComponent.getConfig(); 136 if (commentServiceConfig != null) { 137 RelationManager relationManager = Framework.getService(RelationManager.class); 138 Graph graph = relationManager.getGraph(commentServiceConfig.graphName, session); 139 List<Statement> statements = graph.getStatements(); 140 checkShutdownRequested(migrationContext); 141 142 processBatched(migrationContext, BATCH_SIZE, statements, 143 statement -> migrateCommentsFromRelationToProperty(session, relationManager, commentServiceConfig, 144 statement), 145 "Migrating comments from Relation to Property"); 146 reportProgress("Done Migrating from Relation to Property", statements.size(), statements.size()); 147 } 148 } 149 150 /** 151 * @since 11.1 152 */ 153 protected void migrateCommentsFromRelationToProperty(CoreSession session, RelationManager relationManager, 154 CommentServiceConfig config, Statement statement) { 155 Map<String, Object> ctxMap = Collections.singletonMap(ResourceAdapter.CORE_SESSION_CONTEXT_KEY, session); 156 QNameResourceImpl object = (QNameResourceImpl) statement.getObject(); 157 DocumentModel parent = (DocumentModel) relationManager.getResourceRepresentation(config.documentNamespace, 158 object, ctxMap); 159 160 QNameResourceImpl subject = (QNameResourceImpl) statement.getSubject(); 161 DocumentModel comment = (DocumentModel) relationManager.getResourceRepresentation(config.commentNamespace, 162 subject, ctxMap); 163 164 if (parent != null && comment != null) { 165 comment.putContextData(DISABLE_NOTIFICATION_SERVICE, TRUE); // Remove notifications 166 comment.setPropertyValue(COMMENT_PARENT_ID_PROPERTY, parent.getId()); 167 session.saveDocument(comment); 168 } else if (parent == null && comment == null) { 169 log.warn("Documents {} and {} do not exist, they cannot be migrated", object.getLocalName(), 170 subject.getLocalName()); 171 } else { 172 log.warn("Document {} does not exist, it cannot be migrated", 173 () -> parent == null ? object.getLocalName() : subject.getLocalName()); 174 } 175 176 Graph graph = relationManager.getGraph(config.graphName, session); 177 graph.remove(statement); 178 179 } 180 181 /** 182 * @since 11.1 183 */ 184 protected void migrateSessionPropertyToSecured(CoreSession session, MigrationContext migrationContext) { 185 List<String> comments = getUnsecuredCommentIds(session); 186 // For migration purpose and to avoid any duplication, we should rely mainly on `TreeCommentManager` 187 // For 10.10 (backward compatibility) Framework.getService(CommentManager.class) will return 188 // `PropertyCommentManager` the new location should be computed by the `TreeCommentManager` 189 TreeCommentManager treeCommentManager = new TreeCommentManager(); 190 processBatched(migrationContext, BATCH_SIZE, comments, 191 comment -> migrateCommentsFromPropertyToSecured(session, treeCommentManager, new IdRef(comment)), 192 "Migrating comments from Property to Secured"); 193 194 // All comments were migrated, now we can delete the empty folders 195 int totalOfComments = comments.size(); 196 int nbeOfUnMigratedComments = getUnsecuredCommentIds(session).size(); 197 if (nbeOfUnMigratedComments == 0) { 198 session.query(GET_COMMENTS_FOLDERS_QUERY).forEach((folder -> session.removeDocument(folder.getRef()))); 199 200 IdRef[] documentsToRemove = session.queryProjection(GET_COMMENTS_FOLDERS_QUERY, 0, 0) 201 .stream() 202 .map(m -> new IdRef((String) m.get(ECM_UUID))) 203 .toArray(IdRef[]::new); 204 205 session.removeDocuments(documentsToRemove); 206 207 reportProgress("Done Migrating from Property to Secured", totalOfComments, totalOfComments); 208 } else { 209 // For some reason some comments still not migrated (for example a comment without a comment:parentId) 210 // in this case we just rename the Comments folder (see getCommentFolders) 211 log.warn( 212 "Some comments have not been migrated, see logs for more information. The folder containing these comments will be renamed to {}", 213 UNMIGRATED_COMMENTS_FOLDER_NAME); 214 215 session.query(GET_COMMENTS_FOLDERS_QUERY).forEach(docModel -> { 216 session.move(docModel.getRef(), null, UNMIGRATED_COMMENTS_FOLDER_NAME); 217 docModel.putContextData(DISABLE_NOTIFICATION_SERVICE, TRUE); 218 session.saveDocument(docModel); 219 }); 220 session.save(); 221 222 reportProgress("Done Migrating from Property to Secured", totalOfComments - nbeOfUnMigratedComments, 223 totalOfComments); 224 } 225 } 226 227 /** 228 * @since 11.1 229 */ 230 protected void migrateCommentsFromPropertyToSecured(CoreSession session, TreeCommentManager treeCommentManager, 231 IdRef commentIdRef) { 232 DocumentModel commentDoc = session.getDocument(commentIdRef); 233 String parentId = (String) commentDoc.getPropertyValue(COMMENT_PARENT_ID_PROPERTY); 234 if (StringUtils.isEmpty(parentId)) { 235 log.warn( 236 "The comment document model with IdRef: {} cannot be migrated, because its 'comment:parentId' is not defined", 237 commentIdRef); 238 return; 239 } 240 241 DocumentRef parentDocRef = new IdRef(parentId); 242 if (!session.exists(parentDocRef)) { 243 log.warn( 244 "The comment document model with IdRef: {} cannot be migrated, because its parent: {} cannot be found", 245 commentIdRef, parentId); 246 return; 247 } 248 249 DocumentModel parentDoc = session.getDocument(parentDocRef); 250 251 DocumentRef destination = treeCommentManager.getLocationRefOfCommentCreation(session, parentDoc); 252 253 // Move the commentIdRef under the new destination (under the `Comments` folder in the case of the first comment 254 // or under the comment itself in the case of reply) 255 session.move(commentIdRef, destination, null); 256 257 // Remove notifications 258 commentDoc.putContextData(DISABLE_NOTIFICATION_SERVICE, TRUE); 259 260 // Strip ACLs 261 ACP acp = session.getACP(commentIdRef); 262 // Case where a comment is on a placeless document which has no acp 263 if (acp != null) { 264 acp.removeACL(LOCAL_ACL); 265 session.setACP(commentIdRef, acp, true); 266 } 267 268 session.saveDocument(commentDoc); 269 session.save(); 270 } 271 272 /** 273 * @since 11.1 274 */ 275 protected boolean hasUnsecuredComments(CoreSession session) { 276 List<String> folderIds = getCommentFolders(session); 277 return folderIds.stream().anyMatch(folderId -> session.hasChildren(new IdRef(folderId))); 278 } 279 280 /** 281 * @since 11.1 282 */ 283 protected List<String> getUnsecuredCommentIds(CoreSession session) { 284 List<String> parentIds = getCommentFolders(session); 285 if (parentIds.isEmpty()) { 286 return Collections.emptyList(); 287 } 288 289 String query = String.format("SELECT %s FROM Comment WHERE %s IN (%s)", ECM_UUID, ECM_PARENTID, 290 buildInClause(parentIds)); 291 292 return session.queryProjection(query, 0, 0) 293 .stream() 294 .map(m -> (String) m.get(ECM_UUID)) 295 .collect(Collectors.toList()); 296 } 297 298 /** 299 * @since 11.1 300 */ 301 protected List<String> getCommentFolders(CoreSession session) { 302 List<String> parentIds = new ArrayList<>(); 303 304 List<String> rootCommentsFolderIds = session.queryProjection(GET_COMMENTS_FOLDERS_QUERY, 0, 0) 305 .stream() 306 .map(entry -> (String) entry.get(ECM_UUID)) 307 .collect(Collectors.toList()); 308 309 // According to the case: 310 // Comments created using PropertyCommentManager are stored directly under `Comments` hidden folder (one per 311 // domain) 312 // Comments created using CommentManagerImpl are stored under subfolder (named with a timestamp) of the 313 // `Comments` folder 314 if (!rootCommentsFolderIds.isEmpty()) { 315 // Get all `Comments` hidden folders 316 String query = String.format("SELECT %s FROM Document WHERE %s IN (%s) AND %s = '%s'", ECM_UUID, 317 ECM_PARENTID, buildInClause(rootCommentsFolderIds), ECM_PRIMARYTYPE, HIDDEN_FOLDER_TYPE); 318 319 // Get timestamp subfolders (see CommentManagerImpl#getCommentPathList) 320 List<String> timestampCommentFoldersIds = session.queryProjection(query, 0, 0) 321 .stream() 322 .map(entry -> (String) entry.get(ECM_UUID)) 323 .collect(Collectors.toList()); 324 parentIds.addAll(rootCommentsFolderIds); 325 parentIds.addAll(timestampCommentFoldersIds); 326 } 327 return parentIds; 328 } 329 330 @Override 331 public String probeState() { 332 List<String> repositoryNames = Framework.getService(RepositoryService.class).getRepositoryNames(); 333 Set<String> probes = repositoryNames.stream().map(this::probeRepository).collect(Collectors.toSet()); 334 if (probes.contains(MIGRATION_STATE_RELATION)) { 335 return MIGRATION_STATE_RELATION; 336 } else if (probes.contains(MIGRATION_STATE_PROPERTY)) { 337 return MIGRATION_STATE_PROPERTY; 338 } 339 return MIGRATION_STATE_SECURED; 340 } 341 342 @Override 343 public void notifyStatusChange() { 344 CommentService commentComponent = (CommentService) Framework.getRuntime().getComponent(CommentService.NAME); 345 commentComponent.invalidateCommentManagerImplementation(); 346 } 347 348 /** 349 * @since 11.1 350 */ 351 protected String buildInClause(List<String> parentIds) { 352 return parentIds.stream().collect(Collectors.joining("', '", "'", "'")); 353 } 354}