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}