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