/*
* (C) Copyright 2015-2017 Nuxeo (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Florent Guillaume
*/
package org.nuxeo.ecm.core.blob;
import java.io.IOException;
import java.io.InputStream;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.Blob;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.NuxeoException;
import org.nuxeo.ecm.core.blob.BlobDispatcher.BlobDispatch;
import org.nuxeo.ecm.core.blob.binary.BinaryGarbageCollector;
import org.nuxeo.ecm.core.blob.binary.BinaryManager;
import org.nuxeo.ecm.core.blob.binary.BinaryManagerStatus;
import org.nuxeo.ecm.core.model.Document;
import org.nuxeo.ecm.core.model.Document.BlobAccessor;
import org.nuxeo.ecm.core.model.Repository;
import org.nuxeo.ecm.core.repository.RepositoryService;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.model.ComponentContext;
import org.nuxeo.runtime.model.ComponentInstance;
import org.nuxeo.runtime.model.DefaultComponent;
/**
* Implementation of the service managing {@link Blob}s associated to a {@link Document} or a repository.
*
* @since 9.2
*/
public class DocumentBlobManagerComponent extends DefaultComponent implements DocumentBlobManager {
private static final Log log = LogFactory.getLog(DocumentBlobManagerComponent.class);
protected static final String XP = "configuration";
protected static BlobDispatcher DEFAULT_BLOB_DISPATCHER = new DefaultBlobDispatcher();
protected Deque<BlobDispatcherDescriptor> blobDispatcherDescriptorsRegistry = new LinkedList<>();
@Override
public void deactivate(ComponentContext context) {
blobDispatcherDescriptorsRegistry.clear();
}
@Override
public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
if (XP.equals(extensionPoint)) {
if (contribution instanceof BlobDispatcherDescriptor) {
registerBlobDispatcher((BlobDispatcherDescriptor) contribution);
} else {
throw new NuxeoException("Invalid descriptor: " + contribution.getClass());
}
} else {
throw new NuxeoException("Invalid extension point: " + extensionPoint);
}
}
@Override
public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
if (XP.equals(extensionPoint)) {
if (contribution instanceof BlobDispatcherDescriptor) {
unregisterBlobDispatcher((BlobDispatcherDescriptor) contribution);
}
}
}
protected void registerBlobDispatcher(BlobDispatcherDescriptor descr) {
blobDispatcherDescriptorsRegistry.add(descr);
}
protected void unregisterBlobDispatcher(BlobDispatcherDescriptor descr) {
blobDispatcherDescriptorsRegistry.remove(descr);
}
protected BlobDispatcher getBlobDispatcher() {
BlobDispatcherDescriptor descr = blobDispatcherDescriptorsRegistry.peekLast();
if (descr == null) {
return DEFAULT_BLOB_DISPATCHER;
}
return descr.getBlobDispatcher();
}
protected BlobProvider getBlobProvider(String providerId) {
return Framework.getService(BlobManager.class).getBlobProvider(providerId);
}
protected DocumentBlobProvider getDocumentBlobProvider(Blob blob) {
BlobProvider blobProvider = Framework.getService(BlobManager.class).getBlobProvider(blob);
if (blobProvider instanceof DocumentBlobProvider) {
return (DocumentBlobProvider) blobProvider;
}
return null;
}
/**
* {@inheritDoc}
* <p>
* The {@link BlobInfo} (coming from the database) contains the blob key, which may or may not be prefixed by a blob
* provider id.
*/
@Override
public Blob readBlob(BlobInfo blobInfo, String repositoryName) throws IOException {
String key = blobInfo.key;
if (key == null) {
return null;
}
BlobProvider blobProvider = getBlobProvider(key, repositoryName);
if (blobProvider == null) {
throw new NuxeoException("No registered blob provider for key: " + key);
}
return blobProvider.readBlob(blobInfo);
}
protected BlobProvider getBlobProvider(String key, String repositoryName) {
int colon = key.indexOf(':');
String providerId;
if (colon < 0) {
// no prefix, use the blob dispatcher to find the blob provider id
providerId = getBlobDispatcher().getBlobProvider(repositoryName);
} else {
// use the prefix as blob provider id
providerId = key.substring(0, colon);
}
return getBlobProvider(providerId);
}
/**
* {@inheritDoc}
* <p>
* If the blob is managed and already uses the provider that's expected for this blob and document, there is no need
* to recompute a key. Otherwise, go through the blob provider.
*/
@Override
public String writeBlob(Blob blob, Document doc, String xpath) throws IOException {
BlobDispatcher blobDispatcher = getBlobDispatcher();
BlobDispatch dispatch = null;
if (blob instanceof ManagedBlob) {
ManagedBlob managedBlob = (ManagedBlob) blob;
String currentProviderId = managedBlob.getProviderId();
// is it something we don't have to dispatch?
if (!blobDispatcher.getBlobProviderIds().contains(currentProviderId)) {
// not something we have to dispatch, reuse the key
return managedBlob.getKey();
}
dispatch = blobDispatcher.getBlobProvider(doc, blob, xpath);
if (dispatch.providerId.equals(currentProviderId)) {
// same provider, just reuse the key
return managedBlob.getKey();
}
}
if (dispatch == null) {
dispatch = blobDispatcher.getBlobProvider(doc, blob, xpath);
}
BlobProvider blobProvider = getBlobProvider(dispatch.providerId);
if (blobProvider == null) {
throw new NuxeoException("No registered blob provider with id: " + dispatch.providerId);
}
String key = blobProvider.writeBlob(blob);
if (dispatch.addPrefix) {
key = dispatch.providerId + ':' + key;
}
return key;
}
@Override
public InputStream getConvertedStream(Blob blob, String mimeType, DocumentModel doc) throws IOException {
DocumentBlobProvider blobProvider = getDocumentBlobProvider(blob);
if (blobProvider == null) {
return null;
}
return blobProvider.getConvertedStream((ManagedBlob) blob, mimeType, doc);
}
protected void freezeVersion(BlobAccessor accessor, Document doc) {
Blob blob = accessor.getBlob();
DocumentBlobProvider blobProvider = getDocumentBlobProvider(blob);
if (blobProvider == null) {
return;
}
try {
Blob newBlob = blobProvider.freezeVersion((ManagedBlob) blob, doc);
if (newBlob != null) {
accessor.setBlob(newBlob);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void freezeVersion(Document doc) {
// finds all blobs, then ask their providers if there's anything to do on check in
doc.visitBlobs(accessor -> freezeVersion(accessor, doc));
}
@Override
public void notifyChanges(Document doc, Set<String> xpaths) {
getBlobDispatcher().notifyChanges(doc, xpaths);
}
// find which GCs to use
// only GC the binary managers to which we dispatch blobs
protected List<BinaryGarbageCollector> getGarbageCollectors() {
List<BinaryGarbageCollector> gcs = new LinkedList<>();
for (String providerId : getBlobDispatcher().getBlobProviderIds()) {
BlobProvider blobProvider = getBlobProvider(providerId);
BinaryManager binaryManager = blobProvider.getBinaryManager();
if (binaryManager != null) {
gcs.add(binaryManager.getGarbageCollector());
}
}
return gcs;
}
@Override
public BinaryManagerStatus garbageCollectBinaries(boolean delete) {
List<BinaryGarbageCollector> gcs = getGarbageCollectors();
// start gc
long start = System.currentTimeMillis();
for (BinaryGarbageCollector gc : gcs) {
gc.start();
}
// in all repositories, mark referenced binaries
// the marking itself will call back into the appropriate gc's mark method
RepositoryService repositoryService = Framework.getService(RepositoryService.class);
for (String repositoryName : repositoryService.getRepositoryNames()) {
Repository repository = repositoryService.getRepository(repositoryName);
repository.markReferencedBinaries();
}
// stop gc
BinaryManagerStatus globalStatus = new BinaryManagerStatus();
for (BinaryGarbageCollector gc : gcs) {
gc.stop(delete);
BinaryManagerStatus status = gc.getStatus();
globalStatus.numBinaries += status.numBinaries;
globalStatus.sizeBinaries += status.sizeBinaries;
globalStatus.numBinariesGC += status.numBinariesGC;
globalStatus.sizeBinariesGC += status.sizeBinariesGC;
}
globalStatus.gcDuration = System.currentTimeMillis() - start;
return globalStatus;
}
@Override
public void markReferencedBinary(String key, String repositoryName) {
BlobProvider blobProvider = getBlobProvider(key, repositoryName);
BinaryManager binaryManager = blobProvider.getBinaryManager();
if (binaryManager != null) {
int colon = key.indexOf(':');
if (colon > 0) {
// if the key is in the "providerId:digest" format, keep only the real digest
key = key.substring(colon + 1);
}
binaryManager.getGarbageCollector().mark(key);
} else {
log.error("Unknown binary manager for key: " + key);
}
}
@Override
public boolean isBinariesGarbageCollectionInProgress() {
for (BinaryGarbageCollector gc : getGarbageCollectors()) {
if (gc.isInProgress()) {
return true;
}
}
return false;
}
}