001/* 002 * (C) Copyright 2017 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 * Florent Guillaume 018 */ 019package org.nuxeo.ecm.platform.tag; 020 021import static java.lang.Boolean.TRUE; 022import static java.util.function.Predicate.isEqual; 023import static org.nuxeo.ecm.core.api.CoreSession.ALLOW_VERSION_WRITE; 024import static org.nuxeo.ecm.core.query.sql.NXQL.ECM_NAME; 025import static org.nuxeo.ecm.core.query.sql.NXQL.ECM_UUID; 026import static org.nuxeo.ecm.platform.ec.notification.NotificationConstants.DISABLE_NOTIFICATION_SERVICE; 027import static org.nuxeo.ecm.platform.tag.FacetedTagService.LABEL_PROPERTY; 028import static org.nuxeo.ecm.platform.tag.FacetedTagService.USERNAME_PROPERTY; 029import static org.nuxeo.ecm.platform.tag.TagConstants.MIGRATION_STATE_FACETS; 030import static org.nuxeo.ecm.platform.tag.TagConstants.MIGRATION_STATE_RELATIONS; 031import static org.nuxeo.ecm.platform.tag.TagConstants.MIGRATION_STEP_RELATIONS_TO_FACETS; 032import static org.nuxeo.ecm.platform.tag.TagConstants.TAGGING_SOURCE_FIELD; 033import static org.nuxeo.ecm.platform.tag.TagConstants.TAG_LIST; 034 035import java.io.Serializable; 036import java.util.HashMap; 037import java.util.HashSet; 038import java.util.List; 039import java.util.Map; 040import java.util.Set; 041import java.util.stream.Collectors; 042 043import org.apache.commons.logging.Log; 044import org.apache.commons.logging.LogFactory; 045import org.nuxeo.ecm.core.api.CoreSession; 046import org.nuxeo.ecm.core.api.DocumentModel; 047import org.nuxeo.ecm.core.api.DocumentNotFoundException; 048import org.nuxeo.ecm.core.api.IdRef; 049import org.nuxeo.ecm.core.api.NuxeoException; 050import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; 051import org.nuxeo.ecm.core.migrator.AbstractRepositoryMigrator; 052import org.nuxeo.ecm.core.repository.RepositoryService; 053import org.nuxeo.runtime.api.Framework; 054import org.nuxeo.runtime.migration.MigrationService.MigrationContext; 055 056/** 057 * Migrator of tags. 058 * 059 * @since 9.3 060 */ 061public class TagsMigrator extends AbstractRepositoryMigrator { 062 063 private static final Log log = LogFactory.getLog(TagsMigrator.class); 064 065 protected static final String QUERY_TAGGING = "SELECT ecm:uuid, relation:source, ecm:name, dc:creator FROM Tagging WHERE ecm:isProxy = 0"; 066 067 /** 068 * A label + username. 069 * 070 * @since 9.3 071 */ 072 protected static class Tag { 073 074 protected final String label; 075 076 protected final String username; 077 078 public Tag(String label, String username) { 079 this.label = label; 080 this.username = username; 081 } 082 083 @Override 084 public int hashCode() { 085 final int prime = 31; 086 int result = 1; 087 result = prime * result + ((label == null) ? 0 : label.hashCode()); 088 result = prime * result + ((username == null) ? 0 : username.hashCode()); 089 return result; 090 } 091 092 @Override 093 public boolean equals(Object obj) { 094 if (this == obj) { 095 return true; 096 } 097 if (obj == null) { 098 return false; 099 } 100 if (!(obj instanceof Tag)) { 101 return false; 102 } 103 Tag other = (Tag) obj; 104 if (label == null) { 105 if (other.label != null) { 106 return false; 107 } 108 } else if (!label.equals(other.label)) { 109 return false; 110 } 111 if (username == null) { 112 if (other.username != null) { 113 return false; 114 } 115 } else if (!username.equals(other.username)) { 116 return false; 117 } 118 return true; 119 } 120 121 @Override 122 public String toString() { 123 return "Tag(" + label + "," + username + ")"; 124 } 125 } 126 127 protected static final int BATCH_SIZE = 50; 128 129 @Override 130 public void notifyStatusChange() { 131 TagServiceImpl tagService = Framework.getService(TagServiceImpl.class); 132 tagService.invalidateTagServiceImplementation(); 133 } 134 135 @Override 136 public String probeState() { 137 List<String> repositoryNames = Framework.getService(RepositoryService.class).getRepositoryNames(); 138 if (repositoryNames.stream().map(this::probeRepository).anyMatch(isEqual(MIGRATION_STATE_RELATIONS))) { 139 return MIGRATION_STATE_RELATIONS; 140 } 141 return MIGRATION_STATE_FACETS; 142 } 143 144 @Override 145 protected String probeSession(CoreSession session) { 146 // finds if there are any taggings 147 List<Map<String, Serializable>> taggingMaps = session.queryProjection(QUERY_TAGGING, 1, 0); // limit 1 148 if (!taggingMaps.isEmpty()) { 149 return MIGRATION_STATE_RELATIONS; 150 } else { 151 return MIGRATION_STATE_FACETS; 152 } 153 } 154 155 @Override 156 public void run(String step, MigrationContext migrationContext) { 157 if (!MIGRATION_STEP_RELATIONS_TO_FACETS.equals(step)) { 158 throw new NuxeoException("Unknown migration step: " + step); 159 } 160 this.migrationContext = migrationContext; 161 reportProgress("Initializing", 0, -1); // unknown 162 List<String> repositoryNames = Framework.getService(RepositoryService.class).getRepositoryNames(); 163 try { 164 repositoryNames.forEach(repoName -> migrateRepository(step, migrationContext, repoName)); 165 } catch (MigrationShutdownException e) { 166 return; 167 } 168 } 169 170 @Override 171 protected void migrateSession(String step, MigrationContext migrationContext, CoreSession session) { 172 // query all tagging 173 List<Map<String, Serializable>> taggingMaps = session.queryProjection(QUERY_TAGGING, -1, 0); 174 175 checkShutdownRequested(migrationContext); 176 177 // query all tags we'll have to remove too 178 String tagSql = "SELECT ecm:uuid FROM Tag WHERE ecm:isProxy = 0"; 179 List<Map<String, Serializable>> tagMaps = session.queryProjection(tagSql, -1, 0); 180 181 checkShutdownRequested(migrationContext); 182 183 // compute all tagged documents and their tag label and username 184 Map<String, Set<Tag>> docTags = new HashMap<>(); 185 for (Map<String, Serializable> map : taggingMaps) { 186 String docId = (String) map.get(TAGGING_SOURCE_FIELD); 187 String label = (String) map.get(ECM_NAME); 188 String username = (String) map.get("dc:creator"); 189 Tag tag = new Tag(label, username); 190 docTags.computeIfAbsent(docId, key -> new HashSet<>()).add(tag); 191 } 192 // compute all Tagging doc ids 193 Set<String> taggingIds = taggingMaps.stream() // 194 .map(map -> (String) map.get(ECM_UUID)) 195 .collect(Collectors.toSet()); 196 // compute all Tag doc ids 197 Set<String> tagIds = tagMaps.stream() // 198 .map(map -> (String) map.get(ECM_UUID)) 199 .collect(Collectors.toSet()); 200 201 checkShutdownRequested(migrationContext); 202 203 // recreate all doc tags 204 processBatched(migrationContext, BATCH_SIZE, docTags.entrySet(), es -> addTags(session, es.getKey(), es.getValue()), 205 "Creating new tags"); 206 207 // delete all Tagging and Tag documents 208 processBatched(migrationContext, BATCH_SIZE, taggingIds, docId -> removeDocument(session, docId), 209 "Deleting old Tagging documents"); 210 processBatched(migrationContext, BATCH_SIZE, tagIds, docId -> removeDocument(session, docId), "Deleting old Tag documents"); 211 212 reportProgress("Done", docTags.size(), docTags.size()); 213 } 214 215 protected void removeDocument(CoreSession session, String docId) { 216 try { 217 session.removeDocument(new IdRef(docId)); 218 } catch (DocumentNotFoundException e) { 219 // ignore document that was already removed, or whose type is unknown 220 return; 221 } 222 } 223 224 protected void addTags(CoreSession session, String docId, Set<Tag> tags) { 225 DocumentModel doc; 226 try { 227 if (docId == null) { 228 log.debug("docId found null in addTags"); 229 // ignore null docId 230 return; 231 } 232 doc = session.getDocument(new IdRef(docId)); 233 } catch (DocumentNotFoundException e) { 234 // ignore document that was already removed, or whose type is unknown 235 return; 236 } 237 addTags(doc, tags); 238 } 239 240 @SuppressWarnings("unchecked") 241 protected void addTags(DocumentModel doc, Set<Tag> tags) { 242 if (doc.isProxy()) { 243 // adding tags is not allowed on proxies 244 return; 245 } 246 List<Map<String, Serializable>> tagsList; 247 try { 248 tagsList = (List<Map<String, Serializable>>) doc.getPropertyValue(TAG_LIST); 249 } catch (PropertyNotFoundException e) { 250 // missing facet, cannot add tag 251 return; 252 } 253 boolean changed = false; 254 for (Tag tag : tags) { 255 if (tagsList.stream().noneMatch(t -> tag.label.equals(t.get(LABEL_PROPERTY)))) { 256 Map<String, Serializable> tagMap = new HashMap<>(2); 257 tagMap.put(LABEL_PROPERTY, tag.label); 258 tagMap.put(USERNAME_PROPERTY, tag.username); 259 tagsList.add(tagMap); 260 changed = true; 261 } 262 } 263 if (changed) { 264 doc.putContextData(DISABLE_NOTIFICATION_SERVICE, TRUE); // Remove notifications 265 doc.putContextData(ALLOW_VERSION_WRITE, TRUE); 266 doc.setPropertyValue(TAG_LIST, (Serializable) tagsList); 267 doc.getCoreSession().saveDocument(doc); 268 } 269 } 270 271}