001/*
002 * (C) Copyright 2018 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 *     pierre
018 */
019package org.nuxeo.ecm.core.io.avro;
020
021import java.util.Calendar;
022import java.util.Date;
023import java.util.GregorianCalendar;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028
029import org.apache.avro.Schema;
030import org.apache.avro.Schema.Field;
031import org.apache.avro.generic.GenericData;
032import org.apache.avro.generic.GenericRecord;
033import org.nuxeo.common.utils.Path;
034import org.nuxeo.ecm.core.api.DocumentModel;
035import org.nuxeo.ecm.core.api.DocumentRef;
036import org.nuxeo.ecm.core.api.IdRef;
037import org.nuxeo.ecm.core.api.impl.DocumentModelImpl;
038import org.nuxeo.ecm.core.api.model.Property;
039import org.nuxeo.runtime.RuntimeServiceException;
040import org.nuxeo.runtime.api.Framework;
041import org.nuxeo.runtime.avro.AvroMapper;
042import org.nuxeo.runtime.avro.AvroService;
043
044/**
045 * @since 10.2
046 */
047public class DocumentModelMapper extends AvroMapper<DocumentModel, GenericRecord> {
048
049    public DocumentModelMapper(AvroService service) {
050        super(service);
051    }
052
053    @Override
054    public DocumentModel fromAvro(Schema schema, GenericRecord input) {
055        if (!AvroConstants.DOCUMENT_MODEL.equals(getLogicalType(schema))) {
056            throw new RuntimeServiceException("Schema does not match DocumentModel");
057        }
058        DocumentModel doc = documentModelFromAvro(input);
059        GenericRecord documentTypeRecord = (GenericRecord) input.get(AvroConstants.DOCUMENT_TYPE);
060        for (Field schemaField : documentTypeRecord.getSchema().getFields()) {
061            GenericRecord schemaRecord = (GenericRecord) documentTypeRecord.get(schemaField.name());
062            List<Field> fields = schemaField.schema().getFields();
063            Map<String, Object> data = new HashMap<>(fields.size());
064            for (Field field : fields) {
065                data.put(service.decodeName(field.name()),
066                        service.fromAvro(field.schema(), Property.class, schemaRecord.get(field.name())));
067            }
068            // set properties with privilege to be able to set secure properties
069            Framework.doPrivileged(() -> doc.setProperties(service.decodeName(schemaField.name()), data));
070        }
071        return doc;
072    }
073
074    @Override
075    public GenericRecord toAvro(Schema schema, DocumentModel input) {
076        GenericRecord record = new GenericData.Record(schema);
077        if (AvroConstants.DOCUMENT_MODEL.equals(getLogicalType(schema))) {
078            documentModelToAvro(schema, input, record);
079        } else if (AvroConstants.DOCUMENT_TYPE.equals(getLogicalType(schema))) {
080            for (Field field : schema.getFields()) {
081                record.put(field.name(), service.toAvro(field.schema(), input));
082            }
083        } else {
084            for (Field field : schema.getFields()) {
085                Property p = input.getProperty(service.decodeName(field.name()));
086                record.put(field.name(), service.toAvro(field.schema(), p));
087            }
088        }
089        return record;
090    }
091
092    @SuppressWarnings("unchecked")
093    protected DocumentModel documentModelFromAvro(GenericRecord input) {
094        String path = (String) input.get(AvroConstants.PATH);
095        String type = (String) input.get(AvroConstants.PRIMARY_TYPE);
096        String uuid = (String) input.get(AvroConstants.UUID);
097        String parentId = (String) input.get(AvroConstants.PARENT_ID);
098        String repositoryName = (String) input.get(AvroConstants.REPOSITORY_NAME);
099        Boolean isProxy = (Boolean) input.get(AvroConstants.IS_PROXY);
100        DocumentRef parentRef = parentId == null ? null : new IdRef(parentId);
101        Set<String> facets = (Set<String>) input.get(AvroConstants.MIXIN_TYPES);
102        DocumentModelImpl doc = new DocumentModelImpl(type, uuid, new Path(path), null, parentRef, null, facets, null,
103                isProxy, null, repositoryName, null);
104        doc.setIsVersion((Boolean) input.get(AvroConstants.IS_VERSION));
105        doc.prefetchCurrentLifecycleState((String) input.get(AvroConstants.CURRENT_LIFE_CYCLE_STATE));
106        Boolean isRecord = (Boolean) input.get(AvroConstants.IS_RECORD);
107        if (isRecord) {
108            doc.makeRecord();
109            Long retainUntilMillis = (Long) input.get(AvroConstants.RETAIN_UNTIL);
110            if (retainUntilMillis != null) {
111                Calendar retainUntil = Calendar.getInstance();
112                retainUntil.setTimeInMillis(retainUntilMillis);
113                doc.setRetainUntil(retainUntil);
114            }
115            Boolean hasLegalHold = (Boolean) input.get(AvroConstants.HAS_LEGAL_HOLD);
116            doc.setLegalHold(hasLegalHold);
117        }
118        return doc;
119    }
120
121    protected void documentModelToAvro(Schema schema, DocumentModel doc, GenericRecord record) {
122        record.put(AvroConstants.UUID, doc.getId());
123        record.put(AvroConstants.NAME, doc.getName());
124        record.put(AvroConstants.TITLE, doc.getTitle());
125        record.put(AvroConstants.PATH, doc.getPathAsString());
126        record.put(AvroConstants.REPOSITORY_NAME, doc.getRepositoryName());
127        record.put(AvroConstants.PRIMARY_TYPE, doc.getType());
128        DocumentRef parentRef = doc.getParentRef();
129        if (parentRef != null) {
130            record.put(AvroConstants.PARENT_ID, parentRef.toString());
131        }
132        record.put(AvroConstants.CURRENT_LIFE_CYCLE_STATE, doc.getCurrentLifeCycleState());
133        if (doc.isVersion()) {
134            record.put(AvroConstants.VERSION_LABEL, doc.getVersionLabel());
135            record.put(AvroConstants.VERSION_VERSIONABLE_ID, doc.getVersionSeriesId());
136        }
137        record.put(AvroConstants.IS_PROXY, doc.isProxy());
138        record.put(AvroConstants.IS_TRASHED, doc.isTrashed());
139        record.put(AvroConstants.IS_VERSION, doc.isVersion());
140        record.put(AvroConstants.IS_CHECKEDIN, !doc.isCheckedOut());
141        record.put(AvroConstants.IS_LATEST_VERSION, doc.isLatestVersion());
142        record.put(AvroConstants.IS_LATEST_MAJOR_VERSION, doc.isLatestMajorVersion());
143        record.put(AvroConstants.IS_RECORD, doc.isRecord());
144        Calendar retainUntil = doc.getRetainUntil();
145        if (retainUntil != null) {
146            record.put(AvroConstants.RETAIN_UNTIL, retainUntil.toInstant().toEpochMilli());
147        }
148        record.put(AvroConstants.HAS_LEGAL_HOLD, doc.hasLegalHold());
149        record.put(AvroConstants.CHANGE_TOKEN, doc.getChangeToken());
150        if (doc.getPos() != null) {
151            record.put(AvroConstants.POS, doc.getPos());
152        }
153        // facets
154        record.put(AvroConstants.MIXIN_TYPES, doc.getFacets());
155        // document type with schemas
156        record.put(AvroConstants.DOCUMENT_TYPE, service.toAvro(schema.getField(AvroConstants.DOCUMENT_TYPE).schema(), doc));
157        // INFO \\ tags and acls are ignored for now
158    }
159
160}