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