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