001/*
002 * (C) Copyright 2006-2011 Nuxeo SA (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 *     Anahide Tchertchian
018 */
019package org.nuxeo.ecm.core.api.model.impl.primitives;
020
021import java.io.IOException;
022import java.io.Serializable;
023import java.util.HashMap;
024import java.util.Map;
025
026import org.nuxeo.ecm.core.api.Blob;
027import org.nuxeo.ecm.core.api.NuxeoException;
028import org.nuxeo.ecm.core.api.PropertyException;
029import org.nuxeo.ecm.core.api.blobholder.BlobHolderAdapterService;
030import org.nuxeo.ecm.core.api.model.InvalidPropertyValueException;
031import org.nuxeo.ecm.core.api.model.Property;
032import org.nuxeo.ecm.core.api.model.PropertyConversionException;
033import org.nuxeo.ecm.core.api.model.ReadOnlyPropertyException;
034import org.nuxeo.ecm.core.api.model.impl.MapProperty;
035import org.nuxeo.ecm.core.schema.types.Field;
036import org.nuxeo.runtime.api.Framework;
037
038/**
039 * Property handling an external blob: create/edit is done from a map, and the value returned is a blob.
040 * <p>
041 * Create/edit from a blob is not handled, and the blob uri cannot be retrieved from the blob (no api for now).
042 *
043 * @author Anahide Tchertchian
044 */
045public class ExternalBlobProperty extends MapProperty {
046
047    private static final long serialVersionUID = 1L;
048
049    // constants based on core-types.xsd fields. XXX Should be in model
050    public static final String ENCODING = "encoding";
051
052    public static final String MIME_TYPE = "mime-type";
053
054    public static final String FILE_NAME = "name";
055
056    public static final String DIGEST = "digest";
057
058    public static final String LENGTH = "length";
059
060    public static final String URI = "uri";
061
062    public ExternalBlobProperty(Property parent, Field field, int flags) {
063        super(parent, field, flags);
064    }
065
066    public ExternalBlobProperty(Property parent, Field field) {
067        super(parent, field);
068    }
069
070    @Override
071    @SuppressWarnings("unchecked")
072    public void init(Serializable value) throws PropertyException {
073        if (value == null) {
074            // IGNORE null values - properties will be
075            // considered PHANTOMS
076            return;
077        }
078        Map<String, Serializable> map;
079        if (value instanceof Map) {
080            map = (Map<String, Serializable>) value;
081        } else if (value instanceof Blob) {
082            // XXX: workaround: get the uri from the local prop because it's not on
083            // the Blob
084            map = getMapFromBlobWithUri((Blob) value);
085        } else {
086            throw new PropertyException("Invalid value for external blob (map or blob needed): " + value);
087        }
088        for (Entry<String, Serializable> entry : map.entrySet()) {
089            Property property = get(entry.getKey());
090            property.init(entry.getValue());
091        }
092        removePhantomFlag();
093    }
094
095    @Override
096    @SuppressWarnings("unchecked")
097    public Serializable internalGetValue() throws PropertyException {
098        Object mapValue = super.internalGetValue();
099        if (mapValue instanceof Map) {
100            Blob blob = getBlobFromMap((Map<String, Object>) mapValue);
101            if (blob != null && !(blob instanceof Serializable)) {
102                throw new PropertyException("Blob is not serializable: " + blob);
103            }
104            return (Serializable) blob;
105        } else if (mapValue != null) {
106            throw new PropertyException("Invalid value for external blob (map needed): " + mapValue);
107        }
108        return null;
109    }
110
111    @Override
112    @SuppressWarnings("unchecked")
113    public <T> T getValue(Class<T> type) throws PropertyException {
114        Object value = super.internalGetValue();
115        if (value == null) {
116            return null;
117        }
118        if (value instanceof Map) {
119            if (Map.class.isAssignableFrom(type)) {
120                return (T) value;
121            }
122            if (Blob.class.isAssignableFrom(type)) {
123                return (T) getBlobFromMap((Map<String, Object>) value);
124            }
125        }
126        throw new PropertyConversionException(value.getClass(), type);
127    }
128
129    @Override
130    public Serializable getValueForWrite() throws PropertyException {
131        return (Serializable) getValue(Map.class);
132    }
133
134    /**
135     * Overridden to be able to set a blob from a given map.
136     * <p>
137     * Take care of not overriding the uri if set as this information is not on the blob.
138     *
139     * @throws PropertyException if one of the sub properties throws an exception or if trying to set values to a blob
140     *             without any already existing uri set.
141     */
142    @Override
143    @SuppressWarnings("unchecked")
144    public void setValue(Object value) throws PropertyException {
145        if (!isContainer()) { // if not a container use default setValue()
146            super.setValue(value);
147            return;
148        }
149        if (isReadOnly() || isSecuredForContext()) {
150            throw new ReadOnlyPropertyException(
151                    String.format("Cannot set the value of property: %s since it is readonly", getXPath()));
152        }
153        if (value == null) {
154            remove();
155            return; // TODO how to treat nulls?
156        }
157        if (value instanceof Blob) {
158            Property property = get(URI);
159            Object uri = property.getValue();
160            if (uri == null) {
161                throw new PropertyException("Cannot set blob properties without " + "an existing uri set");
162            }
163            // only update additional properties
164            Map<String, Serializable> map = getMapFromBlob((Blob) value);
165            for (Entry<String, Serializable> entry : map.entrySet()) {
166                String entryKey = entry.getKey();
167                if (entryKey != URI) {
168                    property = get(entryKey);
169                    property.setValue(entry.getValue());
170                }
171            }
172            return;
173        }
174        if (!(value instanceof Map)) {
175            throw new InvalidPropertyValueException(getXPath());
176        }
177        Map<String, Object> map = (Map<String, Object>) value;
178        for (Entry<String, Object> entry : map.entrySet()) {
179            Property property = get(entry.getKey());
180            property.setValue(entry.getValue());
181        }
182    }
183
184    @Override
185    protected boolean isSameValue(Serializable value1, Serializable value2) {
186        // for now, blob property are considered always as dirty when update - see NXP-16322
187        return false;
188    }
189
190    public static Blob getBlobFromMap(Map<String, Object> mapValue) {
191        if (mapValue == null) {
192            return null;
193        }
194        String uri = (String) mapValue.get(URI);
195        if (uri == null || "".equals(uri)) {
196            return null;
197        }
198        String filename = (String) mapValue.get(FILE_NAME);
199        String mimeType = (String) mapValue.get(MIME_TYPE);
200        String encoding = (String) mapValue.get(ENCODING);
201        String digest = (String) mapValue.get(DIGEST);
202        try {
203            BlobHolderAdapterService service = Framework.getService(BlobHolderAdapterService.class);
204            if (service == null) {
205                throw new NuxeoException("BlobHolderAdapterService not found");
206            }
207            Blob blob = service.getExternalBlobForUri(uri);
208            if (filename != null) {
209                blob.setFilename(filename);
210            }
211            blob.setMimeType(mimeType);
212            blob.setEncoding(encoding);
213            // TODO maybe check if digest is still a match to the retrieved blob
214            blob.setDigest(digest);
215            return blob;
216        } catch (IOException e) {
217            throw new NuxeoException(e);
218        }
219    }
220
221    public Map<String, Serializable> getMapFromBlobWithUri(Blob blob) throws PropertyException {
222        Map<String, Serializable> map = getMapFromBlob(blob);
223        Property property = get(URI);
224        Serializable uri = property.getValue();
225        map.put(URI, uri);
226        return map;
227    }
228
229    public static Map<String, Serializable> getMapFromBlob(Blob blob) {
230        Map<String, Serializable> map = new HashMap<>();
231        if (blob == null) {
232            map.put(FILE_NAME, null);
233            map.put(MIME_TYPE, null);
234            map.put(ENCODING, null);
235            map.put(LENGTH, null);
236            map.put(DIGEST, null);
237        } else {
238            // cannot return uri for blob for now: no edit implemented
239            map.put(FILE_NAME, blob.getFilename());
240            map.put(MIME_TYPE, blob.getMimeType());
241            map.put(ENCODING, blob.getEncoding());
242            map.put(LENGTH, blob.getLength());
243            map.put(DIGEST, blob.getDigest());
244        }
245        return map;
246    }
247
248}