001/* 002 * (C) Copyright 2006-2017 Nuxeo (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 * Nuxeo - initial API and implementation 018 */ 019package org.nuxeo.ecm.platform.filemanager.service.extension; 020 021import static org.nuxeo.ecm.core.api.security.SecurityConstants.ADD_CHILDREN; 022import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_PROPERTIES; 023 024import java.io.IOException; 025import java.util.ArrayList; 026import java.util.List; 027import java.util.regex.Pattern; 028 029import org.apache.commons.lang3.StringUtils; 030import org.nuxeo.ecm.core.api.Blob; 031import org.nuxeo.ecm.core.api.CoreSession; 032import org.nuxeo.ecm.core.api.DocumentModel; 033import org.nuxeo.ecm.core.api.DocumentSecurityException; 034import org.nuxeo.ecm.core.api.NuxeoException; 035import org.nuxeo.ecm.core.api.PathRef; 036import org.nuxeo.ecm.core.api.VersioningOption; 037import org.nuxeo.ecm.core.api.blobholder.BlobHolder; 038import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService; 039import org.nuxeo.ecm.core.blob.BlobManager; 040import org.nuxeo.ecm.core.blob.BlobProvider; 041import org.nuxeo.ecm.platform.filemanager.api.FileImporterContext; 042import org.nuxeo.ecm.platform.filemanager.service.FileManagerService; 043import org.nuxeo.ecm.platform.filemanager.utils.FileManagerUtils; 044import org.nuxeo.ecm.platform.types.Type; 045import org.nuxeo.ecm.platform.types.TypeManager; 046import org.nuxeo.runtime.api.Framework; 047 048/** 049 * File importer abstract class. 050 * <p> 051 * Default file importer behavior. 052 * 053 * @see FileImporter 054 * @author <a href="mailto:[email protected]">Andreas Kalogeropolos</a> 055 */ 056public abstract class AbstractFileImporter implements FileImporter { 057 058 private static final long serialVersionUID = 1L; 059 060 protected String name = ""; 061 062 protected String docType; 063 064 protected transient List<String> filters = new ArrayList<>(); 065 066 protected transient List<Pattern> patterns; 067 068 protected boolean enabled = true; 069 070 protected Integer order = 0; 071 072 public static final String SKIP_UPDATE_AUDIT_LOGGING = "org.nuxeo.filemanager.skip.audit.logging.forupdates"; 073 074 // duplicated from Audit module to avoid circular dependency 075 public static final String DISABLE_AUDIT_LOGGER = "disableAuditLogger"; 076 077 // to be used by plugin implementation to gain access to standard file 078 // creation utility methods without having to lookup the service 079 /** 080 * @deprecated since 10.3, use {@link Framework#getService(Class)} instead if needed 081 */ 082 @Deprecated 083 protected transient FileManagerService fileManagerService; 084 085 @Override 086 public List<String> getFilters() { 087 return filters; 088 } 089 090 @Override 091 public void setFilters(List<String> filters) { 092 this.filters = filters; 093 patterns = new ArrayList<>(); 094 for (String filter : filters) { 095 patterns.add(Pattern.compile(filter)); 096 } 097 } 098 099 @Override 100 public boolean matches(String mimeType) { 101 for (Pattern pattern : patterns) { 102 if (pattern.matcher(mimeType).matches()) { 103 return true; 104 } 105 } 106 return false; 107 } 108 109 @Override 110 public String getName() { 111 return name; 112 } 113 114 @Override 115 public void setName(String name) { 116 this.name = name; 117 } 118 119 @Override 120 public String getDocType() { 121 return docType; 122 } 123 124 @Override 125 public void setDocType(String docType) { 126 this.docType = docType; 127 } 128 129 /** 130 * Gets the doc type to use in the given container. 131 */ 132 protected String getDocType(DocumentModel container) { // NOSONAR 133 return getDocType(); // use XML configuration 134 } 135 136 /** 137 * Default document type to use when the plugin XML configuration does not specify one. 138 * <p> 139 * To implement when the default {@link #createOrUpdate(FileImporterContext)} method is used. 140 */ 141 protected String getDefaultDocType() { 142 throw new UnsupportedOperationException(); 143 } 144 145 /** 146 * Whether document overwrite is detected by checking title or filename. 147 * <p> 148 * To implement when the default {@link #createOrUpdate(FileImporterContext)} method is used. 149 */ 150 protected boolean isOverwriteByTitle() { 151 throw new UnsupportedOperationException(); 152 } 153 154 /** 155 * Creates the document (sets its properties). {@link #updateDocument} will be called after this. 156 * <p> 157 * Default implementation sets the title. 158 */ 159 protected void createDocument(DocumentModel doc, String title) { 160 doc.setPropertyValue("dc:title", title); 161 } 162 163 /** 164 * Tries to update the document <code>doc</code> with the blob <code>content</code>. 165 * <p> 166 * Returns <code>true</code> if the document is really updated. 167 * 168 * @since 7.1 169 */ 170 protected boolean updateDocumentIfPossible(DocumentModel doc, Blob content) { 171 updateDocument(doc, content); 172 return true; 173 } 174 175 /** 176 * Updates the document (sets its properties). 177 * <p> 178 * Default implementation sets the content. 179 */ 180 protected void updateDocument(DocumentModel doc, Blob content) { 181 doc.getAdapter(BlobHolder.class).setBlob(content); 182 } 183 184 protected Blob getBlob(DocumentModel doc) { 185 return doc.getAdapter(BlobHolder.class).getBlob(); 186 } 187 188 @Override 189 public boolean isOneToMany() { 190 return false; 191 } 192 193 @Override 194 public DocumentModel create(CoreSession session, Blob content, String path, boolean overwrite, String fullname, 195 TypeManager typeService) throws IOException { 196 FileImporterContext context = FileImporterContext.builder(session, content, path) 197 .overwrite(overwrite) 198 .fileName(fullname) 199 .build(); 200 return createOrUpdate(context); 201 } 202 203 @Override 204 public DocumentModel createOrUpdate(FileImporterContext context) throws IOException { 205 CoreSession session = context.getSession(); 206 String path = getNearestContainerPath(session, context.getParentPath()); 207 DocumentModel container = session.getDocument(new PathRef(path)); 208 String targetDocType = getDocType(container); // from override or descriptor 209 if (targetDocType == null) { 210 targetDocType = getDefaultDocType(); 211 } 212 doSecurityCheck(session, path, targetDocType); 213 214 Blob blob = context.getBlob(); 215 String filename = FileManagerUtils.fetchFileName( 216 StringUtils.defaultIfBlank(context.getFileName(), blob.getFilename())); 217 String title = FileManagerUtils.fetchTitle(filename); 218 blob.setFilename(filename); 219 // look for an existing document with same title or filename 220 DocumentModel doc; 221 if (isOverwriteByTitle()) { 222 doc = FileManagerUtils.getExistingDocByTitle(session, path, title); 223 } else { 224 doc = FileManagerUtils.getExistingDocByFileName(session, path, filename); 225 } 226 if (context.isOverwrite() && doc != null) { 227 Blob previousBlob = getBlob(doc); 228 // check that previous blob allows overwrite 229 if (previousBlob != null) { 230 BlobProvider blobProvider = Framework.getService(BlobManager.class).getBlobProvider(previousBlob); 231 if (blobProvider != null && !blobProvider.supportsUserUpdate()) { 232 throw new DocumentSecurityException("Cannot overwrite blob"); 233 } 234 } 235 // update data 236 boolean isDocumentUpdated = updateDocumentIfPossible(doc, blob); 237 if (!isDocumentUpdated) { 238 return null; 239 } 240 if (Framework.isBooleanPropertyTrue(SKIP_UPDATE_AUDIT_LOGGING)) { 241 // skip the update event if configured to do so 242 doc.putContextData(DISABLE_AUDIT_LOGGER, true); 243 } 244 if (context.isPersistDocument()) { 245 // save 246 doc.putContextData(CoreSession.SOURCE, "fileimporter-" + getName()); 247 doc = doc.getCoreSession().saveDocument(doc); 248 session.save(); 249 } 250 } else { 251 // create document model 252 doc = session.createDocumentModel(targetDocType); 253 createDocument(doc, title); 254 // set path 255 PathSegmentService pss = Framework.getService(PathSegmentService.class); 256 doc.setPathInfo(path, pss.generatePathSegment(doc)); 257 // update data 258 updateDocument(doc, blob); 259 if (context.isPersistDocument()) { 260 // create 261 doc.putContextData(CoreSession.SOURCE, "fileimporter-" + getName()); 262 doc = session.createDocument(doc); 263 session.save(); 264 } 265 } 266 return doc; 267 } 268 269 /** 270 * Avoid checkin for a 0-length blob. Microsoft-WebDAV-MiniRedir first creates a 0-length file and then locks it 271 * before putting the real file. But we don't want this first placeholder to cause a versioning event. 272 * 273 * @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning 274 * behaviors from importers 275 */ 276 @Deprecated 277 protected boolean skipCheckInForBlob(Blob blob) { 278 return blob == null || blob.getLength() == 0; 279 } 280 281 /** 282 * @deprecated since 10.3, use {@link Framework#getService(Class)} instead if needed 283 */ 284 @Deprecated 285 public FileManagerService getFileManagerService() { 286 return fileManagerService; 287 } 288 289 /** 290 * @deprecated since 10.3, use {@link Framework#getService(Class)} instead if needed 291 */ 292 @Deprecated 293 @Override 294 public void setFileManagerService(FileManagerService fileManagerService) { 295 this.fileManagerService = fileManagerService; 296 } 297 298 @Override 299 public void setEnabled(boolean enabled) { 300 this.enabled = enabled; 301 } 302 303 @Override 304 public boolean isEnabled() { 305 return enabled; 306 } 307 308 @Override 309 public Integer getOrder() { 310 return order; 311 } 312 313 @Override 314 public void setOrder(Integer order) { 315 this.order = order; 316 } 317 318 @Override 319 public int compareTo(FileImporter other) { 320 Integer otherOrder = other.getOrder(); 321 if (order == null && otherOrder == null) { 322 return 0; 323 } else if (order == null) { 324 return 1; 325 } else if (otherOrder == null) { 326 return -1; 327 } 328 return order.compareTo(otherOrder); 329 } 330 331 /** 332 * Returns nearest container path 333 * <p> 334 * If given path points to a folderish document, return it. Else, return parent path. 335 */ 336 protected String getNearestContainerPath(CoreSession documentManager, String path) { 337 DocumentModel currentDocument = documentManager.getDocument(new PathRef(path)); 338 if (!currentDocument.isFolder()) { 339 path = path.substring(0, path.lastIndexOf('/')); 340 } 341 return path; 342 } 343 344 /** 345 * @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning 346 * behaviors from importers 347 */ 348 @Deprecated 349 protected void checkIn(DocumentModel doc) { 350 VersioningOption option = fileManagerService.getVersioningOption(); 351 if (option != null && option != VersioningOption.NONE && doc.isCheckedOut()) { 352 doc.checkIn(option, null); 353 } 354 } 355 356 /** 357 * @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning 358 * behaviors from importers 359 */ 360 @Deprecated 361 protected void checkInAfterAdd(DocumentModel doc) { 362 if (fileManagerService.doVersioningAfterAdd()) { 363 checkIn(doc); 364 } 365 } 366 367 /** 368 * @since 10.10 369 */ 370 protected void doSecurityCheck(CoreSession documentManager, String path, String typeName) { 371 doSecurityCheck(documentManager, path, typeName, Framework.getService(TypeManager.class)); 372 } 373 374 protected void doSecurityCheck(CoreSession documentManager, String path, String typeName, TypeManager typeService) { 375 // perform the security checks 376 PathRef containerRef = new PathRef(path); 377 if (!documentManager.hasPermission(containerRef, READ_PROPERTIES) 378 || !documentManager.hasPermission(containerRef, ADD_CHILDREN)) { 379 throw new DocumentSecurityException("Not enough rights to create folder"); 380 } 381 DocumentModel container = documentManager.getDocument(containerRef); 382 383 Type containerType = typeService.getType(container.getType()); 384 if (containerType == null) { 385 return; 386 } 387 388 if (!typeService.isAllowedSubType(typeName, container.getType(), container)) { 389 throw new NuxeoException(String.format("Cannot create document of type %s in container with type %s", 390 typeName, containerType.getId())); 391 } 392 } 393 394}