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) 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) 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()) {
150            throw new ReadOnlyPropertyException(getPath());
151        }
152        if (value == null) {
153            remove();
154            return; // TODO how to treat nulls?
155        }
156        if (value instanceof Blob) {
157            Property property = get(URI);
158            Object uri = property.getValue();
159            if (uri == null) {
160                throw new PropertyException("Cannot set blob properties without " + "an existing uri set");
161            }
162            // only update additional properties
163            Map<String, Serializable> map = getMapFromBlob((Blob) value);
164            for (Entry<String, Serializable> entry : map.entrySet()) {
165                String entryKey = entry.getKey();
166                if (entryKey != URI) {
167                    property = get(entryKey);
168                    property.setValue(entry.getValue());
169                }
170            }
171            return;
172        }
173        if (!(value instanceof Map)) {
174            throw new InvalidPropertyValueException(getPath());
175        }
176        Map<String, Object> map = (Map<String, Object>) value;
177        for (Entry<String, Object> entry : map.entrySet()) {
178            Property property = get(entry.getKey());
179            property.setValue(entry.getValue());
180        }
181    }
182
183    @Override
184    protected boolean isSameValue(Serializable value1, Serializable value2) {
185        // for now, blob property are considered always as dirty when update - see NXP-16322
186        return false;
187    }
188
189    public static Blob getBlobFromMap(Map<String, Object> mapValue) {
190        if (mapValue == null) {
191            return null;
192        }
193        String uri = (String) mapValue.get(URI);
194        if (uri == null || "".equals(uri)) {
195            return null;
196        }
197        String filename = (String) mapValue.get(FILE_NAME);
198        String mimeType = (String) mapValue.get(MIME_TYPE);
199        String encoding = (String) mapValue.get(ENCODING);
200        String digest = (String) mapValue.get(DIGEST);
201        try {
202            BlobHolderAdapterService service = Framework.getService(BlobHolderAdapterService.class);
203            if (service == null) {
204                throw new NuxeoException("BlobHolderAdapterService not found");
205            }
206            Blob blob = service.getExternalBlobForUri(uri);
207            if (filename != null) {
208                blob.setFilename(filename);
209            }
210            blob.setMimeType(mimeType);
211            blob.setEncoding(encoding);
212            // TODO maybe check if digest is still a match to the retrieved blob
213            blob.setDigest(digest);
214            return blob;
215        } catch (IOException e) {
216            throw new NuxeoException(e);
217        }
218    }
219
220    public Map<String, Serializable> getMapFromBlobWithUri(Blob blob) throws PropertyException {
221        Map<String, Serializable> map = getMapFromBlob(blob);
222        Property property = get(URI);
223        Serializable uri = property.getValue();
224        map.put(URI, uri);
225        return map;
226    }
227
228    public static Map<String, Serializable> getMapFromBlob(Blob blob) {
229        Map<String, Serializable> map = new HashMap<String, Serializable>();
230        if (blob == null) {
231            map.put(FILE_NAME, null);
232            map.put(MIME_TYPE, null);
233            map.put(ENCODING, null);
234            map.put(LENGTH, null);
235            map.put(DIGEST, null);
236        } else {
237            // cannot return uri for blob for now: no edit implemented
238            map.put(FILE_NAME, blob.getFilename());
239            map.put(MIME_TYPE, blob.getMimeType());
240            map.put(ENCODING, blob.getEncoding());
241            map.put(LENGTH, blob.getLength());
242            map.put(DIGEST, blob.getDigest());
243        }
244        return map;
245    }
246
247}