001/*
002 * (C) Copyright 2015 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 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.core.blob;
020
021import java.io.ByteArrayInputStream;
022import java.io.IOException;
023import java.io.InputStream;
024import java.net.URI;
025import java.util.Collections;
026import java.util.HashMap;
027import java.util.Map;
028import java.util.Set;
029
030import javax.servlet.http.HttpServletRequest;
031
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.nuxeo.ecm.core.api.Blob;
035import org.nuxeo.ecm.core.api.NuxeoException;
036import org.nuxeo.ecm.core.blob.binary.BinaryBlobProvider;
037import org.nuxeo.ecm.core.blob.binary.BinaryManager;
038import org.nuxeo.runtime.model.ComponentContext;
039import org.nuxeo.runtime.model.ComponentInstance;
040import org.nuxeo.runtime.model.DefaultComponent;
041import org.nuxeo.runtime.model.SimpleContributionRegistry;
042
043/**
044 * Implementation of the service managing the storage and retrieval of {@link Blob}s, through internally-registered
045 * {@link BlobProvider}s.
046 *
047 * @since 7.2
048 */
049public class BlobManagerComponent extends DefaultComponent implements BlobManager {
050
051    private static final Log log = LogFactory.getLog(BlobManagerComponent.class);
052
053    protected static final String XP = "configuration";
054
055    public static final String DEFAULT_ID = "default";
056
057    /**
058     * Blob providers whose id starts with this prefix are automatically marked transient.
059     *
060     * @since 10.10
061     * @see BlobProvider#isTransient
062     */
063    public static final String TRANSIENT_ID_PREFIX = "transient";
064
065    protected BlobProviderDescriptorRegistry blobProviderDescriptorsRegistry = new BlobProviderDescriptorRegistry();
066
067    protected Map<String, BlobProvider> blobProviders = new HashMap<>();
068
069    protected static class BlobProviderDescriptorRegistry extends SimpleContributionRegistry<BlobProviderDescriptor> {
070
071        @Override
072        public String getContributionId(BlobProviderDescriptor contrib) {
073            return contrib.name;
074        }
075
076        @Override
077        public BlobProviderDescriptor clone(BlobProviderDescriptor orig) {
078            return new BlobProviderDescriptor(orig);
079        }
080
081        @Override
082        public void merge(BlobProviderDescriptor src, BlobProviderDescriptor dst) {
083            dst.merge(src);
084        }
085
086        @Override
087        public boolean isSupportingMerge() {
088            return true;
089        }
090
091        public void clear() {
092            currentContribs.clear();
093        }
094
095        public BlobProviderDescriptor getBlobProviderDescriptor(String id) {
096            return getCurrentContribution(id);
097        }
098
099        public Set<String> getBlobProviderIds() {
100            return currentContribs.keySet();
101        }
102    }
103
104    @Override
105    public void deactivate(ComponentContext context) {
106        blobProviderDescriptorsRegistry.clear();
107        // close each blob provider
108        for (BlobProvider blobProvider : blobProviders.values()) {
109            blobProvider.close();
110        }
111        blobProviders.clear();
112    }
113
114    @Override
115    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
116        if (XP.equals(extensionPoint)) {
117            if (contribution instanceof BlobProviderDescriptor) {
118                registerBlobProvider((BlobProviderDescriptor) contribution);
119            } else {
120                throw new NuxeoException("Invalid descriptor: " + contribution.getClass());
121            }
122        } else {
123            throw new NuxeoException("Invalid extension point: " + extensionPoint);
124        }
125    }
126
127    @Override
128    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
129        if (XP.equals(extensionPoint)) {
130            if (contribution instanceof BlobProviderDescriptor) {
131                unregisterBlobProvider((BlobProviderDescriptor) contribution);
132            }
133        }
134    }
135
136    // public for tests
137    public void registerBlobProvider(BlobProviderDescriptor descr) {
138        closeOldBlobProvider(descr.name);
139        blobProviderDescriptorsRegistry.addContribution(descr);
140        // We can't look up the blob provider right now to initialize it, the platform has not started yet
141        // and the blob provider initialization may require a connection that has not been registered yet.
142    }
143
144    // public for tests
145    public void unregisterBlobProvider(BlobProviderDescriptor descr) {
146        closeOldBlobProvider(descr.name);
147        blobProviderDescriptorsRegistry.removeContribution(descr);
148    }
149
150    /**
151     * We're about to change something about a contributed blob provider. Close the old one.
152     */
153    protected synchronized void closeOldBlobProvider(String id) {
154        BlobProvider blobProvider = blobProviders.remove(id);
155        if (blobProvider != null) {
156            blobProvider.close();
157        }
158    }
159
160    @Override
161    public synchronized BlobProvider getBlobProvider(String providerId) {
162        BlobProvider blobProvider = blobProviders.get(providerId);
163        if (blobProvider == null) {
164            BlobProviderDescriptor descr = blobProviderDescriptorsRegistry.getBlobProviderDescriptor(providerId);
165            if (descr == null) {
166                return null;
167            }
168            Class<?> klass = descr.klass;
169            Map<String, String> properties = descr.properties;
170            try {
171                if (BlobProvider.class.isAssignableFrom(klass)) {
172                    @SuppressWarnings("unchecked")
173                    Class<? extends BlobProvider> blobProviderClass = (Class<? extends BlobProvider>) klass;
174                    blobProvider = blobProviderClass.newInstance();
175                } else if (BinaryManager.class.isAssignableFrom(klass)) {
176                    @SuppressWarnings("unchecked")
177                    Class<? extends BinaryManager> binaryManagerClass = (Class<? extends BinaryManager>) klass;
178                    BinaryManager binaryManager = binaryManagerClass.newInstance();
179                    blobProvider = new BinaryBlobProvider(binaryManager);
180                } else {
181                    throw new RuntimeException("Unknown class for blob provider: " + klass);
182                }
183            } catch (ReflectiveOperationException e) {
184                throw new RuntimeException(e);
185            }
186            // make it transient if needed
187            if (providerId.startsWith(TRANSIENT_ID_PREFIX)) {
188                descr.properties.put(BlobProviderDescriptor.TRANSIENT, "true");
189            }
190            try {
191                blobProvider.initialize(providerId, properties);
192            } catch (IOException e) {
193                throw new RuntimeException(e);
194            }
195            blobProviders.put(providerId, blobProvider);
196        }
197        return blobProvider;
198    }
199
200    @Override
201    public synchronized BlobProvider getBlobProviderWithNamespace(String providerId) {
202        BlobProvider blobProvider = getBlobProvider(providerId);
203        if (blobProvider != null) {
204            return blobProvider;
205        }
206        // create and register a blob provider from the "default" configuration
207        BlobProviderDescriptor defaultDescr = blobProviderDescriptorsRegistry.getBlobProviderDescriptor(DEFAULT_ID);
208        if (defaultDescr == null) {
209            throw new NuxeoException("Missing configuration for default blob provider");
210        }
211        // copy
212        BlobProviderDescriptor descr = new BlobProviderDescriptor(defaultDescr);
213        // set new name and namespace
214        descr.name = providerId;
215        descr.properties.put(BlobProviderDescriptor.NAMESPACE, providerId);
216        // register and return it
217        registerBlobProvider(descr);
218        return getBlobProvider(providerId);
219    }
220
221    @Override
222    public BlobProvider getBlobProvider(Blob blob) {
223        if (!(blob instanceof ManagedBlob)) {
224            return null;
225        }
226        ManagedBlob managedBlob = (ManagedBlob) blob;
227        return getBlobProvider(managedBlob.getProviderId());
228    }
229
230    @Override
231    public InputStream getStream(Blob blob) throws IOException {
232        BlobProvider blobProvider = getBlobProvider(blob);
233        if (blobProvider == null) {
234            return null;
235        }
236        try {
237            return blobProvider.getStream((ManagedBlob) blob);
238        } catch (IOException e) {
239            // we don't want to crash everything if the remote file cannot be accessed
240            log.error("Failed to access file: " + ((ManagedBlob) blob).getKey(), e);
241            return new ByteArrayInputStream(new byte[0]);
242        }
243    }
244
245    @Override
246    public InputStream getThumbnail(Blob blob) throws IOException {
247        BlobProvider blobProvider = getBlobProvider(blob);
248        if (blobProvider == null) {
249            return null;
250        }
251        return blobProvider.getThumbnail((ManagedBlob) blob);
252    }
253
254    @Override
255    public URI getURI(Blob blob, UsageHint hint, HttpServletRequest servletRequest) throws IOException {
256        BlobProvider blobProvider = getBlobProvider(blob);
257        if (blobProvider == null) {
258            return null;
259        }
260        return blobProvider.getURI((ManagedBlob) blob, hint, servletRequest);
261    }
262
263    @Override
264    public Map<String, URI> getAvailableConversions(Blob blob, UsageHint hint) throws IOException {
265        BlobProvider blobProvider = getBlobProvider(blob);
266        if (blobProvider == null) {
267            return Collections.emptyMap();
268        }
269        return blobProvider.getAvailableConversions((ManagedBlob) blob, hint);
270    }
271
272    @Override
273    public synchronized Map<String, BlobProvider> getBlobProviders() {
274        Set<String> blobProviderIds = blobProviderDescriptorsRegistry.getBlobProviderIds();
275        if (blobProviders.size() != blobProviderIds.size()) {
276            // register all providers
277            for (String id : blobProviderIds) {
278                getBlobProvider(id); // instantiate and initialize
279            }
280        }
281        return blobProviders;
282    }
283
284}