/*
* (C) Copyright 2006-2011 Nuxeo SA (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.opencmis.impl.server;
import static org.apache.chemistry.opencmis.commons.impl.Constants.RENDITION_NONE;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
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.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.servlet.ServletContext;
import org.apache.chemistry.opencmis.client.api.OperationContext;
import org.apache.chemistry.opencmis.commons.BasicPermissions;
import org.apache.chemistry.opencmis.commons.PropertyIds;
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.MutableAce;
import org.apache.chemistry.opencmis.commons.data.MutableAcl;
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.AccessControlEntryImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlListImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlPrincipalDataImpl;
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.server.CmisService;
import org.apache.chemistry.opencmis.commons.spi.BindingsObjectFactory;
import org.apache.commons.lang.StringUtils;
import org.nuxeo.ecm.core.api.Blob;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.IterableQueryResult;
import org.nuxeo.ecm.core.api.PropertyException;
import org.nuxeo.ecm.core.api.security.ACE;
import org.nuxeo.ecm.core.api.security.ACL;
import org.nuxeo.ecm.core.api.security.ACP;
import org.nuxeo.ecm.core.api.security.SecurityConstants;
import org.nuxeo.ecm.core.api.security.impl.ACPImpl;
import org.nuxeo.ecm.core.opencmis.impl.util.ListUtils;
import org.nuxeo.ecm.core.opencmis.impl.util.SimpleImageInfo;
import org.nuxeo.ecm.platform.rendition.Rendition;
import org.nuxeo.ecm.platform.rendition.service.RenditionDefinition;
import org.nuxeo.ecm.platform.rendition.service.RenditionService;
import org.nuxeo.runtime.api.Framework;
/**
* Nuxeo implementation of a CMIS {@link ObjectData}, backed by a {@link DocumentModel}.
*/
public class NuxeoObjectData implements ObjectData {
public static final String REND_STREAM_ICON = "nuxeo:icon";
public static final String REND_KIND_CMIS_THUMBNAIL = "cmis:thumbnail";
public static final String REND_STREAM_RENDITION_PREFIX = "nuxeo:rendition:";
public static final String REND_KIND_NUXEO_RENDITION = "nuxeo:rendition";
/**
* Property to determine whether all renditions provide a computed size and length.
*
* @since 7.4
*/
public static final String RENDITION_COMPUTE_INFO_PROP = "org.nuxeo.cmis.computeRenditionInfo";
/**
* Default for {@value #RENDITION_COMPUTE_INFO_PROP}.
*
* @since 7.4
*/
public static final String RENDITION_COMPUTE_INFO_DEFAULT = "false";
public CmisService service;
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 List<TypeDefinition> secondaryTypes;
/** type + secondaryTypes */
private List<TypeDefinition> allTypes;
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;
private NuxeoCmisService nuxeoCmisService;
public NuxeoObjectData(CmisService service, DocumentModel doc, String filter, Boolean includeAllowableActions,
IncludeRelationships includeRelationships, String renditionFilter, Boolean includePolicyIds,
Boolean includeAcl, ExtensionsData extension) {
this.service = service;
this.doc = doc;
propertyIds = getPropertyIdsFromFilter(filter);
this.includeAllowableActions = includeAllowableActions;
this.includeRelationships = includeRelationships;
this.renditionFilter = renditionFilter;
this.includePolicyIds = includePolicyIds;
this.includeAcl = includeAcl;
nuxeoCmisService = NuxeoCmisService.extractFromCmisService(service);
type = nuxeoCmisService.getTypeManager().getTypeDefinition(NuxeoTypeHelper.mappedId(doc.getType()));
secondaryTypes = new ArrayList<>();
for (String secondaryTypeId : NuxeoPropertyData.getSecondaryTypeIds(doc)) {
TypeDefinition td = nuxeoCmisService.getTypeManager().getTypeDefinition(secondaryTypeId);
if (td != null) {
secondaryTypes.add(td);
} // else doc has old facet not declared in types anymore, ignore
}
allTypes = new ArrayList<>(1 + secondaryTypes.size());
allTypes.add(type);
allTypes.addAll(secondaryTypes);
callContext = nuxeoCmisService.callContext;
}
protected NuxeoObjectData(CmisService service, DocumentModel doc) {
this(service, doc, null, null, null, null, null, null, null);
}
public NuxeoObjectData(CmisService 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 List<TypeDefinition> getTypeDefinitions() {
return allTypes;
}
@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) {
List<PropertyData<?>> props = new ArrayList<PropertyData<?>>();
for (TypeDefinition t : allTypes) {
Map<String, PropertyDefinition<?>> propertyDefinitions = t.getPropertyDefinitions();
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 isRoot = "/".equals(doc.getPathAsString());
boolean canWrite = creation || doc.getCoreSession().hasPermission(doc.getRef(), SecurityConstants.WRITE);
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_TREE);
set.add(Action.CAN_GET_CHILDREN);
if (!isRoot) {
set.add(Action.CAN_GET_FOLDER_PARENT);
}
} else if (isDocument) {
set.add(Action.CAN_GET_CONTENT_STREAM);
set.add(Action.CAN_GET_ALL_VERSIONS);
set.add(Action.CAN_ADD_OBJECT_TO_FOLDER);
set.add(Action.CAN_REMOVE_OBJECT_FROM_FOLDER);
if (doc.isCheckedOut()) {
set.add(Action.CAN_CHECK_IN);
set.add(Action.CAN_CANCEL_CHECK_OUT);
} else {
set.add(Action.CAN_CHECK_OUT);
}
}
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);
} 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 && !isRoot || isDocument) {
// Relationships are not fileable
set.add(Action.CAN_MOVE_OBJECT);
}
if (!isRoot) {
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);
set.add(Action.CAN_CREATE_ITEM);
}
AllowableActionsImpl aa = new AllowableActionsImpl();
aa.setAllowableActions(set);
return aa;
}
@Override
public List<RenditionData> getRenditions() {
if (!needsRenditions(renditionFilter)) {
return Collections.emptyList();
}
return getRenditions(doc, renditionFilter, null, null, callContext);
}
public static boolean needsRenditions(String renditionFilter) {
return !StringUtils.isBlank(renditionFilter) && !RENDITION_NONE.equals(renditionFilter);
}
public static List<RenditionData> getRenditions(DocumentModel doc, String renditionFilter, BigInteger maxItems,
BigInteger skipCount, CallContext callContext) {
try {
List<RenditionData> list = new ArrayList<RenditionData>();
list.addAll(getRenditionServiceRenditions(doc, callContext));
// rendition filter
if (!STAR.equals(renditionFilter)) {
String[] filters = renditionFilter.split(",");
for (Iterator<RenditionData> it = list.iterator(); it.hasNext();) {
RenditionData ren = it.next();
boolean keep = false;
for (String filter : filters) {
if (filter.contains("/")) {
// mimetype
if (filter.endsWith("/*")) {
String typeSlash = filter.substring(0, filter.indexOf('/') + 1);
if (ren.getMimeType().startsWith(typeSlash)) {
keep = true;
break;
}
} else {
if (ren.getMimeType().equals(filter)) {
keep = true;
break;
}
}
} else {
// kind
if (ren.getKind().equals(filter)) {
keep = true;
break;
}
}
}
if (!keep) {
it.remove();
}
}
}
list = ListUtils.batchList(list, maxItems, skipCount, DEFAULT_MAX_RENDITIONS);
return list;
} catch (IOException e) {
throw new CmisRuntimeException(e.toString(), e);
}
}
/**
* @deprecated since 7.3. The thumbnail is now a default rendition, see NXP-16662.
*/
@Deprecated
protected static List<RenditionData> getIconRendition(DocumentModel doc, CallContext callContext)
throws IOException {
String iconPath;
try {
iconPath = (String) doc.getPropertyValue(NuxeoTypeHelper.NX_ICON);
} catch (PropertyException e) {
iconPath = null;
}
InputStream is = getIconStream(iconPath, callContext);
if (is == null) {
return Collections.emptyList();
}
RenditionDataImpl ren = new RenditionDataImpl();
ren.setStreamId(REND_STREAM_ICON);
ren.setKind(REND_KIND_CMIS_THUMBNAIL);
int slash = iconPath.lastIndexOf('/');
String filename = slash == -1 ? iconPath : iconPath.substring(slash + 1);
ren.setTitle(filename);
SimpleImageInfo info = new SimpleImageInfo(is);
ren.setBigLength(BigInteger.valueOf(info.getLength()));
ren.setBigWidth(BigInteger.valueOf(info.getWidth()));
ren.setBigHeight(BigInteger.valueOf(info.getHeight()));
ren.setMimeType(info.getMimeType());
return Collections.<RenditionData> singletonList(ren);
}
/**
* @deprecated since 7.3. The thumbnail is now a default rendition, see NXP-16662.
*/
@Deprecated
public static InputStream getIconStream(String iconPath, CallContext context) {
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);
}
protected static List<RenditionData> getRenditionServiceRenditions(DocumentModel doc, CallContext callContext)
throws IOException {
RenditionService renditionService = Framework.getLocalService(RenditionService.class);
List<RenditionDefinition> defs = renditionService.getAvailableRenditionDefinitions(doc);
List<RenditionData> list = new ArrayList<>(defs.size());
for (RenditionDefinition def : defs) {
if (!def.isVisible()) {
continue;
}
RenditionDataImpl ren = new RenditionDataImpl();
String cmisName = def.getCmisName();
if (StringUtils.isBlank(cmisName)) {
cmisName = REND_STREAM_RENDITION_PREFIX + def.getName();
}
ren.setStreamId(cmisName);
String kind = def.getKind();
ren.setKind(StringUtils.isNotBlank(kind) ? kind : REND_KIND_NUXEO_RENDITION);
ren.setTitle(def.getLabel());
ren.setMimeType(def.getContentType());
boolean computeInfo = Boolean.parseBoolean(
Framework.getProperty(RENDITION_COMPUTE_INFO_PROP, RENDITION_COMPUTE_INFO_DEFAULT));
if (REND_KIND_CMIS_THUMBNAIL.equals(ren.getKind()) || computeInfo) {
Rendition rendition = renditionService.getRendition(doc, def.getName());
Blob blob = rendition.getBlob();
if (blob != null) {
ren.setTitle(blob.getFilename());
SimpleImageInfo info = new SimpleImageInfo(blob.getStream());
ren.setBigLength(BigInteger.valueOf(info.getLength()));
ren.setBigWidth(BigInteger.valueOf(info.getWidth()));
ren.setBigHeight(BigInteger.valueOf(info.getHeight()));
ren.setMimeType(info.getMimeType());
}
}
list.add(ren);
}
return list;
}
@Override
public List<ObjectData> getRelationships() {
return getRelationships(getId(), includeRelationships, nuxeoCmisService);
}
public static List<ObjectData> getRelationships(String id, IncludeRelationships includeRelationships,
NuxeoCmisService service) {
if (includeRelationships == null || includeRelationships == IncludeRelationships.NONE) {
return null;
}
String statement = "SELECT " + PropertyIds.OBJECT_ID + ", " + PropertyIds.BASE_TYPE_ID + ", "
+ PropertyIds.SOURCE_ID + ", " + PropertyIds.TARGET_ID + " FROM "
+ BaseTypeId.CMIS_RELATIONSHIP.value() + " WHERE ";
String qid = "'" + id.replace("'", "''") + "'";
if (includeRelationships != IncludeRelationships.TARGET) {
statement += PropertyIds.SOURCE_ID + " = " + qid;
}
if (includeRelationships == IncludeRelationships.BOTH) {
statement += " OR ";
}
if (includeRelationships != IncludeRelationships.SOURCE) {
statement += PropertyIds.TARGET_ID + " = " + qid;
}
List<ObjectData> list = new ArrayList<ObjectData>();
IterableQueryResult res = null;
try {
Map<String, PropertyDefinition<?>> typeInfo = new HashMap<String, PropertyDefinition<?>>();
res = service.queryAndFetch(statement, false, typeInfo);
for (Map<String, Serializable> map : res) {
list.add(service.makeObjectData(map, typeInfo));
}
} finally {
if (res != null) {
res.close();
}
}
return list;
}
@Override
public Acl getAcl() {
if (!Boolean.TRUE.equals(includeAcl)) {
return null;
}
ACP acp = doc.getACP();
return getAcl(acp, false, nuxeoCmisService);
}
protected static Acl getAcl(ACP acp, boolean onlyBasicPermissions, NuxeoCmisService service) {
if (acp == null) {
acp = new ACPImpl();
}
Boolean exact = Boolean.TRUE;
List<Ace> aces = new ArrayList<Ace>();
for (ACL acl : acp.getACLs()) {
// inherited and non-local ACLs are non-direct
boolean direct = ACL.LOCAL_ACL.equals(acl.getName());
Map<String, Set<String>> permissionMap = new LinkedHashMap<>();
for (ACE ace : acl.getACEs()) {
boolean denied = ace.isDenied();
String username = ace.getUsername();
String permission = ace.getPermission();
if (denied) {
if (SecurityConstants.EVERYONE.equals(username) && SecurityConstants.EVERYTHING.equals(permission)) {
permission = NuxeoCmisService.PERMISSION_NOTHING;
} else {
// we cannot represent this blocking
exact = Boolean.FALSE;
continue;
}
}
Set<String> permissions = permissionMap.get(username);
if (permissions == null) {
permissionMap.put(username, permissions = new LinkedHashSet<String>());
}
// derive CMIS permission from Nuxeo permissions
boolean isBasic = false;
if (service.readPermissions.contains(permission)) { // Read
isBasic = true;
permissions.add(BasicPermissions.READ);
}
if (service.writePermissions.contains(permission)) { // ReadWrite
isBasic = true;
permissions.add(BasicPermissions.WRITE);
}
if (SecurityConstants.EVERYTHING.equals(permission)) {
isBasic = true;
permissions.add(BasicPermissions.ALL);
}
if (!onlyBasicPermissions) {
permissions.add(permission);
} else if (!isBasic) {
exact = Boolean.FALSE;
}
if (NuxeoCmisService.PERMISSION_NOTHING.equals(permission)) {
break;
}
}
for (Entry<String, Set<String>> en : permissionMap.entrySet()) {
String username = en.getKey();
Set<String> permissions = en.getValue();
if (permissions.isEmpty()) {
continue;
}
MutableAce entry = new AccessControlEntryImpl();
entry.setPrincipal(new AccessControlPrincipalDataImpl(username));
entry.setPermissions(new ArrayList<String>(permissions));
entry.setDirect(direct);
aces.add(entry);
}
}
MutableAcl result = new AccessControlListImpl();
result.setAces(aces);
result.setExact(exact);
return result;
}
@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) {
}
}