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