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 *     Kevin Leturc <kleturc@nuxeo.com>
018 */
019package org.nuxeo.ecm.core.io.impl.transformers;
020
021import java.io.IOException;
022import java.util.List;
023import java.util.Optional;
024import java.util.Set;
025
026import org.apache.commons.lang3.StringUtils;
027import org.dom4j.DocumentHelper;
028import org.dom4j.Element;
029import org.dom4j.Namespace;
030import org.dom4j.QName;
031import org.nuxeo.ecm.core.io.DocumentTransformer;
032import org.nuxeo.ecm.core.io.ExportedDocument;
033import org.nuxeo.ecm.core.schema.PropertyDeprecationHandler;
034import org.nuxeo.ecm.core.schema.SchemaManager;
035import org.nuxeo.ecm.core.schema.TypeConstants;
036import org.nuxeo.ecm.core.schema.types.Field;
037import org.nuxeo.runtime.api.Framework;
038
039/**
040 * This is a {@link DocumentTransformer} which removes property marked as removed in deprecation system.
041 *
042 * @since 9.2
043 */
044public class PropertyDeprecationRemover implements DocumentTransformer {
045
046    protected final SchemaManager schemaManager;
047
048    protected final PropertyDeprecationHandler removeHandler;
049
050    public PropertyDeprecationRemover() {
051        schemaManager = Framework.getService(SchemaManager.class);
052        removeHandler = schemaManager.getRemovedProperties();
053    }
054
055    @Override
056    @SuppressWarnings("unchecked")
057    public boolean transform(ExportedDocument xdoc) throws IOException {
058        Element root = xdoc.getDocument().getRootElement();
059        for (Element schema : (List<Element>) root.elements("schema")) {
060            String schemaName = schema.attributeValue("name");
061            if (removeHandler.hasMarkedProperties(schemaName)) {
062                // schema has removed properties - get them
063                Set<String> props = removeHandler.getProperties(schemaName);
064                for (String prop : props) {
065                    handleProperty(schema, prop);
066                }
067            }
068        }
069        return true;
070    }
071
072    @SuppressWarnings("unchecked")
073    protected void handleProperty(Element schema, String propertyToRemove) {
074        String schemaName = schema.attributeValue("name");
075
076        // build namespace uri for input schema
077        String namespaceURI = "http://www.nuxeo.org/ecm/schemas/" + schemaName + '/';
078        String schemaPrefix = Optional.ofNullable(schema.getNamespaceForURI(namespaceURI))
079                                      .map(Namespace::getPrefix)
080                                      .filter(StringUtils::isNotBlank)
081                                      .map(p -> p + ':')
082                                      .orElse(StringUtils.EMPTY);
083
084        String fallback = removeHandler.getFallback(schemaName, propertyToRemove);
085        // check if the fallback is inside a blob
086        Field fallbackField = schemaManager.getField(schemaPrefix + fallback);
087        if (fallbackField != null && TypeConstants.isContentType(fallbackField.getDeclaringType())) {
088            // as we export blob with the property "filename" instead of "name", which differ from schema definition,
089            // we need to replace the "name" property name
090            fallback = fallback.replace("/name", "/filename");
091        }
092
093        // handle list and other elements
094        int starIndex = propertyToRemove.indexOf('*');
095        if (starIndex < 0) {
096            // removed property is not in a list
097            Element elementToRemove = (Element) schema.selectSingleNode(schemaPrefix + propertyToRemove);
098            if (elementToRemove != null) {
099                moveAndDetachProperty(schema, schema, elementToRemove, fallback);
100            }
101        } else {
102            // removed property is in a list
103            List<Element> elementsToRemove = (List<Element>) (List<?>) schema.selectNodes(schemaPrefix + propertyToRemove);
104            // compute number of times we need to get parent - here we only handle one list level (the last one)
105            // we assume that fallback of a list of complex is inside the same complex property
106            int count = StringUtils.countMatches(propertyToRemove.substring(starIndex), "/");
107            // compute a new fallback
108            String newFallback = fallback;
109            if (newFallback != null) {
110                // we want to skip "*/"
111                newFallback = newFallback.substring(newFallback.lastIndexOf("*/") + 2);
112            }
113            for (Element elementToRemove : elementsToRemove) {
114                Element parent = elementToRemove;
115                for (int i = 0; i < count; i++) {
116                    parent = parent.getParent();
117                }
118                moveAndDetachProperty(schema, parent, elementToRemove, newFallback);
119            }
120        }
121    }
122
123    protected void moveAndDetachProperty(Element schema, Element parent, Element elementToRemove, String fallback) {
124        if (fallback != null) {
125            // as we don't currently handle fallback to another schema, we can move content easily
126            String[] fallbackSegments = fallback.split("/");
127            for (String fallbackSegment : fallbackSegments) {
128                QName qName;
129                Element element;
130                if (parent == schema) {
131                    // first element has a namespace
132                    qName = QName.get(fallbackSegment, schema.getNamespaceForPrefix(schema.attributeValue("name")));
133                } else {
134                    // children don't have namespace
135                    qName = QName.get(fallbackSegment);
136                }
137                element = parent.element(qName);
138                // create element if it doesn't exist
139                if (element == null) {
140                    element = DocumentHelper.createElement(qName);
141                    parent.add(element);
142                }
143                parent = element;
144            }
145            // move content to last element if it doesn't has content - removed properties don't
146            // override fallback
147            if (!parent.hasContent()) {
148                parent.setContent(elementToRemove.content());
149            }
150        }
151        // finally detach removed property
152        elementToRemove.detach();
153    }
154
155}