/*
* Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Florent Guillaume
*/
package org.eclipse.ecr.opencmis.impl.server;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletContext;
import org.apache.chemistry.opencmis.client.api.OperationContext;
import org.apache.chemistry.opencmis.commons.data.Ace;
import org.apache.chemistry.opencmis.commons.data.Acl;
import org.apache.chemistry.opencmis.commons.data.AllowableActions;
import org.apache.chemistry.opencmis.commons.data.ChangeEventInfo;
import org.apache.chemistry.opencmis.commons.data.CmisExtensionElement;
import org.apache.chemistry.opencmis.commons.data.ExtensionsData;
import org.apache.chemistry.opencmis.commons.data.ObjectData;
import org.apache.chemistry.opencmis.commons.data.PolicyIdList;
import org.apache.chemistry.opencmis.commons.data.Properties;
import org.apache.chemistry.opencmis.commons.data.PropertyData;
import org.apache.chemistry.opencmis.commons.data.RenditionData;
import org.apache.chemistry.opencmis.commons.definitions.PropertyDefinition;
import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition;
import org.apache.chemistry.opencmis.commons.enums.Action;
import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships;
import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlListImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.AllowableActionsImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.BindingsObjectFactoryImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PolicyIdListImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.RenditionDataImpl;
import org.apache.chemistry.opencmis.commons.server.CallContext;
import org.apache.chemistry.opencmis.commons.spi.BindingsObjectFactory;
import org.eclipse.ecr.core.api.ClientException;
import org.eclipse.ecr.core.api.DocumentModel;
import org.eclipse.ecr.core.api.model.PropertyException;
import org.eclipse.ecr.core.api.security.SecurityConstants;
import org.eclipse.ecr.opencmis.impl.util.ListUtils;
import org.nuxeo.common.utils.StringUtils;
/**
* Nuxeo implementation of a CMIS {@link ObjectData}, backed by a
* {@link DocumentModel}.
*/
public class NuxeoObjectData implements ObjectData {
public static final String STREAM_ICON = "nx:icon";
public DocumentModel doc;
public boolean creation = false; // TODO
private List<String> propertyIds;
private Boolean includeAllowableActions;
private IncludeRelationships includeRelationships;
private String renditionFilter;
private Boolean includePolicyIds;
private Boolean includeAcl;
private static final BindingsObjectFactory objectFactory = new BindingsObjectFactoryImpl();
private TypeDefinition type;
private static final int CACHE_MAX_SIZE = 10;
private static final int DEFAULT_MAX_RENDITIONS = 20;
/** Cache for Properties objects, which are expensive to create. */
private Map<String, Properties> propertiesCache = new HashMap<String, Properties>();
private CallContext callContext;
public NuxeoObjectData(NuxeoCmisService service, DocumentModel doc,
String filter, Boolean includeAllowableActions,
IncludeRelationships includeRelationships, String renditionFilter,
Boolean includePolicyIds, Boolean includeAcl,
ExtensionsData extension) {
this.doc = doc;
propertyIds = getPropertyIdsFromFilter(filter);
this.includeAllowableActions = includeAllowableActions;
this.includeRelationships = includeRelationships;
this.renditionFilter = renditionFilter;
this.includePolicyIds = includePolicyIds;
this.includeAcl = includeAcl;
type = service.repository.getTypeDefinition(NuxeoTypeHelper.mappedId(doc.getType()));
callContext = service.callContext;
}
protected NuxeoObjectData(NuxeoCmisService service, DocumentModel doc) {
this(service, doc, null, null, null, null, null, null, null);
}
public NuxeoObjectData(NuxeoCmisService service, DocumentModel doc,
OperationContext context) {
this(service, doc, context.getFilterString(),
Boolean.valueOf(context.isIncludeAllowableActions()),
context.getIncludeRelationships(),
context.getRenditionFilterString(),
Boolean.valueOf(context.isIncludePolicies()),
Boolean.valueOf(context.isIncludeAcls()), null);
}
private static final String STAR = "*";
protected static final List<String> STAR_FILTER = Collections.singletonList(STAR);
protected static List<String> getPropertyIdsFromFilter(String filter) {
if (filter == null || filter.length() == 0)
return STAR_FILTER;
else {
List<String> ids = Arrays.asList(filter.split(",\\s*"));
if (ids.contains(STAR)) {
ids = STAR_FILTER;
}
return ids;
}
}
@Override
public String getId() {
return doc.getId();
}
@Override
public BaseTypeId getBaseTypeId() {
return NuxeoTypeHelper.getBaseTypeId(doc);
}
public TypeDefinition getTypeDefinition() {
return type;
}
@Override
public Properties getProperties() {
return getProperties(propertyIds);
}
protected Properties getProperties(List<String> propertyIds) {
// for STAR_FILTER the key is equal to STAR (see limitCacheSize)
String key = StringUtils.join(propertyIds, ',');
Properties properties = propertiesCache.get(key);
if (properties == null) {
Map<String, PropertyDefinition<?>> propertyDefinitions = type.getPropertyDefinitions();
int len = propertyIds == STAR_FILTER ? propertyDefinitions.size()
: propertyIds.size();
List<PropertyData<?>> props = new ArrayList<PropertyData<?>>(len);
for (PropertyDefinition<?> pd : propertyDefinitions.values()) {
if (propertyIds == STAR_FILTER
|| propertyIds.contains(pd.getId())) {
props.add((PropertyData<?>) NuxeoPropertyData.construct(
this, pd, callContext));
}
}
properties = objectFactory.createPropertiesData(props);
limitCacheSize();
propertiesCache.put(key, properties);
}
return properties;
}
/** Limits cache size, always keeps STAR filter. */
protected void limitCacheSize() {
if (propertiesCache.size() >= CACHE_MAX_SIZE) {
Properties sf = propertiesCache.get(STAR);
propertiesCache.clear();
if (sf != null) {
propertiesCache.put(STAR, sf);
}
}
}
public NuxeoPropertyDataBase<?> getProperty(String id) {
// make use of cache
return (NuxeoPropertyDataBase<?>) getProperties(STAR_FILTER).getProperties().get(
id);
}
@Override
public AllowableActions getAllowableActions() {
if (!Boolean.TRUE.equals(includeAllowableActions)) {
return null;
}
return getAllowableActions(doc, creation);
}
public static AllowableActions getAllowableActions(DocumentModel doc,
boolean creation) {
BaseTypeId baseType = NuxeoTypeHelper.getBaseTypeId(doc);
boolean isDocument = baseType == BaseTypeId.CMIS_DOCUMENT;
boolean isFolder = baseType == BaseTypeId.CMIS_FOLDER;
boolean canWrite;
try {
canWrite = creation
|| doc.getCoreSession().hasPermission(doc.getRef(),
SecurityConstants.WRITE);
} catch (ClientException e) {
canWrite = false;
}
Set<Action> set = EnumSet.noneOf(Action.class);
set.add(Action.CAN_GET_OBJECT_PARENTS);
set.add(Action.CAN_GET_PROPERTIES);
if (isFolder) {
set.add(Action.CAN_GET_DESCENDANTS);
set.add(Action.CAN_GET_FOLDER_PARENT);
set.add(Action.CAN_GET_FOLDER_TREE);
set.add(Action.CAN_GET_CHILDREN);
} else if (isDocument) {
set.add(Action.CAN_GET_CONTENT_STREAM);
set.add(Action.CAN_GET_ALL_VERSIONS);
try {
if (doc.isCheckedOut()) {
set.add(Action.CAN_CHECK_IN);
set.add(Action.CAN_CANCEL_CHECK_OUT);
} else {
set.add(Action.CAN_CHECK_OUT);
}
} catch (ClientException e) {
throw new CmisRuntimeException(e.toString(), e);
}
}
if (isFolder || isDocument) {
set.add(Action.CAN_GET_RENDITIONS);
}
if (canWrite) {
if (isFolder) {
set.add(Action.CAN_CREATE_DOCUMENT);
set.add(Action.CAN_CREATE_FOLDER);
set.add(Action.CAN_CREATE_RELATIONSHIP);
set.add(Action.CAN_DELETE_TREE);
set.add(Action.CAN_ADD_OBJECT_TO_FOLDER);
set.add(Action.CAN_REMOVE_OBJECT_FROM_FOLDER);
} else if (isDocument) {
set.add(Action.CAN_SET_CONTENT_STREAM);
set.add(Action.CAN_DELETE_CONTENT_STREAM);
}
set.add(Action.CAN_UPDATE_PROPERTIES);
if (isFolder || isDocument) {
// Relationships are not fileable
set.add(Action.CAN_MOVE_OBJECT);
}
set.add(Action.CAN_DELETE_OBJECT);
}
if (Boolean.FALSE.booleanValue()) {
// TODO
set.add(Action.CAN_GET_OBJECT_RELATIONSHIPS);
set.add(Action.CAN_APPLY_POLICY);
set.add(Action.CAN_REMOVE_POLICY);
set.add(Action.CAN_GET_APPLIED_POLICIES);
set.add(Action.CAN_GET_ACL);
set.add(Action.CAN_APPLY_ACL);
}
AllowableActionsImpl aa = new AllowableActionsImpl();
aa.setAllowableActions(set);
return aa;
}
@Override
public List<RenditionData> getRenditions() {
if (renditionFilter == null || renditionFilter.length() == 0) {
return null;
}
// TODO parse rendition filter; for now returns them all
return getRenditions(doc, null, null, callContext);
}
public static List<RenditionData> getRenditions(DocumentModel doc,
BigInteger maxItems, BigInteger skipCount, CallContext callContext) {
try {
List<RenditionData> list = new ArrayList<RenditionData>();
// first rendition is icon
String iconPath;
try {
iconPath = (String) doc.getPropertyValue(NuxeoTypeHelper.NX_ICON);
} catch (PropertyException e) {
iconPath = null;
}
InputStream is = getIconStream(iconPath, callContext);
if (is != null) {
RenditionDataImpl ren = new RenditionDataImpl();
ren.setStreamId(STREAM_ICON);
ren.setMimeType(getIconMimeType(iconPath));
ren.setKind("cmis:thumbnail");
int slash = iconPath.lastIndexOf('/');
String filename = slash == -1 ? iconPath
: iconPath.substring(slash + 1);
ren.setTitle(filename);
long len = getStreamLength(is);
ren.setBigLength(BigInteger.valueOf(len));
// TODO width, height
// ren.setBigWidth(width);
// ren.setBigHeight(height);
list.add(ren);
}
// TODO other renditions from blob holder secondary blobs
list = ListUtils.batchList(list, maxItems, skipCount, DEFAULT_MAX_RENDITIONS);
return list;
} catch (IOException e) {
throw new CmisRuntimeException(e.toString(), e);
} catch (ClientException e) {
throw new CmisRuntimeException(e.toString(), e);
}
}
public static InputStream getIconStream(String iconPath, CallContext context)
throws ClientException {
if (iconPath == null || iconPath.length() == 0) {
return null;
}
if (!iconPath.startsWith("/")) {
iconPath = '/' + iconPath;
}
ServletContext servletContext = (ServletContext) context.get(CallContext.SERVLET_CONTEXT);
if (servletContext == null) {
throw new CmisRuntimeException("Cannot get servlet context");
}
return servletContext.getResourceAsStream(iconPath);
}
public static long getStreamLength(InputStream is) throws IOException {
byte[] buf = new byte[4096];
long count = 0;
int n;
while ((n = is.read(buf)) != -1) {
count += n;
}
is.close();
return count;
}
protected static String getIconMimeType(String iconPath) {
iconPath = iconPath.toLowerCase();
if (iconPath.endsWith(".gif")) {
return "image/gif";
} else if (iconPath.endsWith(".png")) {
return "image/png";
} else if (iconPath.endsWith(".jpg") || iconPath.endsWith(".jpeg")) {
return "image/jpeg";
} else {
// TODO use NXMimeType service
return "application/octet-stream";
}
}
@Override
public List<ObjectData> getRelationships() {
if (includeRelationships == null
|| includeRelationships == IncludeRelationships.NONE) {
return null;
}
return new ArrayList<ObjectData>(0); // TODO
}
@Override
public Acl getAcl() {
if (!Boolean.TRUE.equals(includeAcl)) {
return null;
}
AccessControlListImpl acl = new AccessControlListImpl();
List<Ace> aces = new ArrayList<Ace>();
acl.setAces(aces);
return acl; // TODO
}
@Override
public Boolean isExactAcl() {
return Boolean.FALSE; // TODO
}
@Override
public PolicyIdList getPolicyIds() {
if (!Boolean.TRUE.equals(includePolicyIds)) {
return null;
}
return new PolicyIdListImpl(); // TODO
}
@Override
public ChangeEventInfo getChangeEventInfo() {
return null;
// throw new UnsupportedOperationException();
}
@Override
public List<CmisExtensionElement> getExtensions() {
return Collections.emptyList();
}
@Override
public void setExtensions(List<CmisExtensionElement> extensions) {
}
}