001/* 002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl-2.1.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Florent Guillaume 016 */ 017package org.nuxeo.ecm.core.blob; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.HashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.Map.Entry; 027import java.util.Set; 028import java.util.regex.Matcher; 029import java.util.regex.Pattern; 030 031import org.apache.commons.logging.Log; 032import org.apache.commons.logging.LogFactory; 033import org.nuxeo.ecm.core.api.Blob; 034import org.nuxeo.ecm.core.api.Blobs; 035import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; 036import org.nuxeo.ecm.core.api.repository.RepositoryManager; 037import org.nuxeo.ecm.core.model.Document; 038import org.nuxeo.ecm.core.model.Document.BlobAccessor; 039import org.nuxeo.runtime.api.Framework; 040 041/** 042 * Default blob dispatcher, that uses the repository name as the blob provider. 043 * <p> 044 * Alternatively, it can be configured through properties to dispatch to a blob provider based on document properties 045 * instead of the repository name. 046 * <p> 047 * The property name is a list of comma-separated clauses, with each clause consisting of a property, an operator and a 048 * value. The property can be a {@link Document} xpath, {@code ecm:repositoryName}, or, to match the current blob being 049 * dispatched, {@code blob:name}, {@code blob:mime-type}, {@code blob:encoding}, {@code blob:digest} or 050 * {@code blob:length}. Comma-separated clauses are ANDed together. The special name {@code default} defines the default 051 * provider, and must be present. 052 * <p> 053 * Available operators between property and value are =, !=, <, and >. 054 * <p> 055 * For example, to dispatch to the "first" provider if dc:format is "video", to the "second" provider if the blob's MIME 056 * type is "video/mp4", to the "third" provider if the lifecycle state is "approved" and the document is in the default 057 * repository, and otherwise to the "fourth" provider: 058 * 059 * <pre> 060 * <property name="dc:format=video">first</property> 061 * <property name="blob:mime-type=video/mp4">second</property> 062 * <property name="ecm:repositoryName=default,ecm:lifeCycleState=approved">third2</property> 063 * <property name="default">fourth</property> 064 * </pre> 065 * 066 * @since 7.3 067 */ 068public class DefaultBlobDispatcher implements BlobDispatcher { 069 070 private static final Log log = LogFactory.getLog(DefaultBlobDispatcher.class); 071 072 protected static final String NAME_DEFAULT = "default"; 073 074 protected static final Pattern NAME_PATTERN = Pattern.compile("(.*)(=|!=|<|>)(.*)"); 075 076 /** Pseudo-property for the repository name. */ 077 protected static final String REPOSITORY_NAME = "ecm:repositoryName"; 078 079 protected static final String BLOB_PREFIX = "blob:"; 080 081 protected static final String BLOB_NAME = "name"; 082 083 protected static final String BLOB_MIME_TYPE = "mime-type"; 084 085 protected static final String BLOB_ENCODING = "encoding"; 086 087 protected static final String BLOB_DIGEST = "digest"; 088 089 protected static final String BLOB_LENGTH = "length"; 090 091 protected enum Op { 092 EQ, NEQ, LT, GT; 093 } 094 095 protected static class Clause { 096 public final String xpath; 097 098 public final Op op; 099 100 public final Object value; 101 102 public Clause(String xpath, Op op, Object value) { 103 this.xpath = xpath; 104 this.op = op; 105 this.value = value; 106 } 107 } 108 109 protected static class Rule { 110 public final List<Clause> clauses; 111 112 public final String providerId; 113 114 public Rule(List<Clause> clauses, String providerId) { 115 this.clauses = clauses; 116 this.providerId = providerId; 117 } 118 } 119 120 // default to true when initialize is not called (default instance) 121 protected boolean useRepositoryName = true; 122 123 protected List<Rule> rules; 124 125 protected Set<String> rulesXPaths; 126 127 protected Set<String> providerIds; 128 129 protected String defaultProviderId; 130 131 @Override 132 public void initialize(Map<String, String> properties) { 133 providerIds = new HashSet<>(); 134 rulesXPaths = new HashSet<>(); 135 rules = new ArrayList<>(); 136 for (Entry<String, String> en : properties.entrySet()) { 137 String clausesString = en.getKey(); 138 String providerId = en.getValue(); 139 providerIds.add(providerId); 140 if (clausesString.equals(NAME_DEFAULT)) { 141 defaultProviderId = providerId; 142 } else { 143 List<Clause> clauses = new ArrayList<Clause>(2); 144 for (String name : clausesString.split(",")) { 145 Matcher m = NAME_PATTERN.matcher(name); 146 if (m.matches()) { 147 String xpath = m.group(1); 148 String ops = m.group(2); 149 Object value = m.group(3); 150 Op op; 151 switch (ops) { 152 case "=": 153 op = Op.EQ; 154 break; 155 case "!=": 156 op = Op.NEQ; 157 break; 158 case "<": 159 op = Op.LT; 160 value = Long.valueOf((String) value); 161 break; 162 case ">": 163 op = Op.GT; 164 value = Long.valueOf((String) value); 165 break; 166 default: 167 log.error("Invalid dispatcher configuration operator: " + ops); 168 continue; 169 } 170 clauses.add(new Clause(xpath, op, value)); 171 rulesXPaths.add(xpath); 172 } else { 173 log.error("Invalid dispatcher configuration property name: " + name); 174 } 175 rules.add(new Rule(clauses, providerId)); 176 } 177 } 178 } 179 useRepositoryName = providerIds.isEmpty(); 180 if (!useRepositoryName && defaultProviderId == null) { 181 log.error("Invalid dispatcher configuration, missing default, configuration will be ignored"); 182 useRepositoryName = true; 183 } 184 } 185 186 @Override 187 public Collection<String> getBlobProviderIds() { 188 if (useRepositoryName) { 189 return Framework.getService(RepositoryManager.class).getRepositoryNames(); 190 } 191 return providerIds; 192 } 193 194 protected String getProviderId(Document doc, Blob blob) { 195 if (useRepositoryName) { 196 return doc.getRepositoryName(); 197 } 198 for (Rule rule : rules) { 199 boolean allClausesMatch = true; 200 for (Clause clause : rule.clauses) { 201 String xpath = clause.xpath; 202 Object value; 203 if (xpath.equals(REPOSITORY_NAME)) { 204 value = doc.getRepositoryName(); 205 } else if (xpath.startsWith(BLOB_PREFIX)) { 206 switch (xpath.substring(BLOB_PREFIX.length())) { 207 case BLOB_NAME: 208 value = blob.getFilename(); 209 break; 210 case BLOB_MIME_TYPE: 211 value = blob.getMimeType(); 212 break; 213 case BLOB_ENCODING: 214 value = blob.getEncoding(); 215 break; 216 case BLOB_DIGEST: 217 value = blob.getDigest(); 218 break; 219 case BLOB_LENGTH: 220 value = Long.valueOf(blob.getLength()); 221 break; 222 default: 223 log.error("Invalid dispatcher configuration property name: " + xpath); 224 continue; 225 } 226 } else { 227 try { 228 value = doc.getValue(xpath); 229 } catch (PropertyNotFoundException e) { 230 try { 231 value = doc.getPropertyValue(xpath); 232 } catch (IllegalArgumentException e2) { 233 continue; 234 } 235 } 236 } 237 boolean match; 238 switch (clause.op) { 239 case EQ: 240 match = String.valueOf(value).equals(clause.value); 241 break; 242 case NEQ: 243 match = !String.valueOf(value).equals(clause.value); 244 break; 245 case LT: 246 if (value == null) { 247 value = Long.valueOf(0); 248 } 249 match = ((Long) value).compareTo((Long) clause.value) < 0; 250 break; 251 case GT: 252 if (value == null) { 253 value = Long.valueOf(0); 254 } 255 match = ((Long) value).compareTo((Long) clause.value) > 0; 256 break; 257 default: 258 throw new AssertionError("notreached"); 259 } 260 allClausesMatch = allClausesMatch && match; 261 if (!allClausesMatch) { 262 break; 263 } 264 } 265 if (allClausesMatch) { 266 return rule.providerId; 267 } 268 } 269 return defaultProviderId; 270 } 271 272 @Override 273 public String getBlobProvider(String repositoryName) { 274 if (useRepositoryName) { 275 return repositoryName; 276 } 277 // useful for legacy blobs created without prefix before dispatch was configured 278 return defaultProviderId; 279 } 280 281 @Override 282 public BlobDispatch getBlobProvider(Document doc, Blob blob) { 283 if (useRepositoryName) { 284 String providerId = doc.getRepositoryName(); 285 return new BlobDispatch(providerId, false); 286 } 287 String providerId = getProviderId(doc, blob); 288 return new BlobDispatch(providerId, true); 289 } 290 291 @Override 292 public void notifyChanges(Document doc, Set<String> xpaths) { 293 if (useRepositoryName) { 294 return; 295 } 296 for (String xpath : rulesXPaths) { 297 if (xpaths.contains(xpath)) { 298 doc.visitBlobs(accessor -> checkBlob(doc, accessor)); 299 return; 300 } 301 } 302 } 303 304 protected void checkBlob(Document doc, BlobAccessor accessor) { 305 Blob blob = accessor.getBlob(); 306 if (!(blob instanceof ManagedBlob)) { 307 return; 308 } 309 // compare current provider with expected 310 String expectedProviderId = getProviderId(doc, blob); 311 if (((ManagedBlob) blob).getProviderId().equals(expectedProviderId)) { 312 return; 313 } 314 // re-write blob 315 // TODO add APIs so that blob providers can copy blobs efficiently from each other 316 Blob newBlob; 317 try (InputStream in = blob.getStream()) { 318 newBlob = Blobs.createBlob(in, blob.getMimeType(), blob.getEncoding()); 319 newBlob.setFilename(blob.getFilename()); 320 newBlob.setDigest(blob.getDigest()); 321 } catch (IOException e) { 322 throw new RuntimeException(e); 323 } 324 accessor.setBlob(newBlob); 325 } 326 327}