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