001 /** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 package org.apache.camel.component.file; 018 019 import java.io.File; 020 import java.util.concurrent.locks.Lock; 021 import java.util.concurrent.locks.ReentrantLock; 022 023 import org.apache.camel.Exchange; 024 import org.apache.camel.Expression; 025 import org.apache.camel.impl.DefaultExchange; 026 import org.apache.camel.impl.DefaultProducer; 027 import org.apache.camel.spi.Language; 028 import org.apache.camel.util.ExchangeHelper; 029 import org.apache.camel.util.FileUtil; 030 import org.apache.camel.util.LRUCache; 031 import org.apache.camel.util.ObjectHelper; 032 import org.apache.camel.util.ServiceHelper; 033 import org.apache.camel.util.StringHelper; 034 import org.slf4j.Logger; 035 import org.slf4j.LoggerFactory; 036 037 /** 038 * Generic file producer 039 */ 040 public class GenericFileProducer<T> extends DefaultProducer { 041 protected final transient Logger log = LoggerFactory.getLogger(getClass()); 042 protected final GenericFileEndpoint<T> endpoint; 043 protected GenericFileOperations<T> operations; 044 // assume writing to 100 different files concurrently at most for the same file producer 045 private final LRUCache<String, Lock> locks = new LRUCache<String, Lock>(100); 046 047 protected GenericFileProducer(GenericFileEndpoint<T> endpoint, GenericFileOperations<T> operations) { 048 super(endpoint); 049 this.endpoint = endpoint; 050 this.operations = operations; 051 } 052 053 public String getFileSeparator() { 054 return File.separator; 055 } 056 057 public String normalizePath(String name) { 058 return FileUtil.normalizePath(name); 059 } 060 061 public void process(Exchange exchange) throws Exception { 062 String target = createFileName(exchange); 063 064 // use lock for same file name to avoid concurrent writes to the same file 065 // for example when you concurrently append to the same file 066 Lock lock; 067 synchronized (locks) { 068 lock = locks.get(target); 069 if (lock == null) { 070 lock = new ReentrantLock(); 071 locks.put(target, lock); 072 } 073 } 074 075 lock.lock(); 076 try { 077 processExchange(exchange, target); 078 } finally { 079 // do not remove as the locks cache has an upper bound 080 // this ensure the locks is appropriate reused 081 lock.unlock(); 082 } 083 } 084 085 /** 086 * Sets the operations to be used. 087 * <p/> 088 * Can be used to set a fresh operations in case of recovery attempts 089 * 090 * @param operations the operations 091 */ 092 public void setOperations(GenericFileOperations<T> operations) { 093 this.operations = operations; 094 } 095 096 /** 097 * Perform the work to process the fileExchange 098 * 099 * @param exchange fileExchange 100 * @param target the target filename 101 * @throws Exception is thrown if some error 102 */ 103 protected void processExchange(Exchange exchange, String target) throws Exception { 104 log.trace("Processing file: {} for exchange: {}", target, exchange); 105 106 try { 107 preWriteCheck(); 108 109 // should we write to a temporary name and then afterwards rename to real target 110 boolean writeAsTempAndRename = ObjectHelper.isNotEmpty(endpoint.getTempFileName()); 111 String tempTarget = null; 112 // remember if target exists to avoid checking twice 113 Boolean targetExists = null; 114 if (writeAsTempAndRename) { 115 // compute temporary name with the temp prefix 116 tempTarget = createTempFileName(exchange, target); 117 118 log.trace("Writing using tempNameFile: {}", tempTarget); 119 120 // cater for file exists option on the real target as 121 // the file operations code will work on the temp file 122 123 // if an existing file already exists what should we do? 124 targetExists = operations.existsFile(target); 125 if (targetExists) { 126 if (endpoint.getFileExist() == GenericFileExist.Ignore) { 127 // ignore but indicate that the file was written 128 log.trace("An existing file already exists: {}. Ignore and do not override it.", target); 129 return; 130 } else if (endpoint.getFileExist() == GenericFileExist.Fail) { 131 throw new GenericFileOperationFailedException("File already exist: " + target + ". Cannot write new file."); 132 } else if (endpoint.isEagerDeleteTargetFile() && endpoint.getFileExist() == GenericFileExist.Override) { 133 // we override the target so we do this by deleting it so the temp file can be renamed later 134 // with success as the existing target file have been deleted 135 log.trace("Eagerly deleting existing file: {}", target); 136 if (!operations.deleteFile(target)) { 137 throw new GenericFileOperationFailedException("Cannot delete file: " + target); 138 } 139 } 140 } 141 142 // delete any pre existing temp file 143 if (operations.existsFile(tempTarget)) { 144 log.trace("Deleting existing temp file: {}", tempTarget); 145 if (!operations.deleteFile(tempTarget)) { 146 throw new GenericFileOperationFailedException("Cannot delete file: " + tempTarget); 147 } 148 } 149 } 150 151 // write/upload the file 152 writeFile(exchange, tempTarget != null ? tempTarget : target); 153 154 // if we did write to a temporary name then rename it to the real 155 // name after we have written the file 156 if (tempTarget != null) { 157 158 // if we should not eager delete the target file then do it now just before renaming 159 if (!endpoint.isEagerDeleteTargetFile() && targetExists 160 && endpoint.getFileExist() == GenericFileExist.Override) { 161 // we override the target so we do this by deleting it so the temp file can be renamed later 162 // with success as the existing target file have been deleted 163 log.trace("Deleting existing file: {}", target); 164 if (!operations.deleteFile(target)) { 165 throw new GenericFileOperationFailedException("Cannot delete file: " + target); 166 } 167 } 168 169 // now we are ready to rename the temp file to the target file 170 log.trace("Renaming file: [{}] to: [{}]", tempTarget, target); 171 boolean renamed = operations.renameFile(tempTarget, target); 172 if (!renamed) { 173 throw new GenericFileOperationFailedException("Cannot rename file from: " + tempTarget + " to: " + target); 174 } 175 } 176 177 // any done file to write? 178 if (endpoint.getDoneFileName() != null) { 179 String doneFileName = endpoint.createDoneFileName(target); 180 ObjectHelper.notEmpty(doneFileName, "doneFileName", endpoint); 181 182 // create empty exchange with empty body to write as the done file 183 Exchange empty = new DefaultExchange(exchange); 184 empty.getIn().setBody(""); 185 186 log.trace("Writing done file: [{}]", doneFileName); 187 // delete any existing done file 188 if (operations.existsFile(doneFileName)) { 189 if (!operations.deleteFile(doneFileName)) { 190 throw new GenericFileOperationFailedException("Cannot delete existing done file: " + doneFileName); 191 } 192 } 193 writeFile(empty, doneFileName); 194 } 195 196 // let's store the name we really used in the header, so end-users 197 // can retrieve it 198 exchange.getIn().setHeader(Exchange.FILE_NAME_PRODUCED, target); 199 } catch (Exception e) { 200 handleFailedWrite(exchange, e); 201 } 202 203 postWriteCheck(); 204 } 205 206 /** 207 * If we fail writing out a file, we will call this method. This hook is 208 * provided to disconnect from servers or clean up files we created (if needed). 209 */ 210 public void handleFailedWrite(Exchange exchange, Exception exception) throws Exception { 211 throw exception; 212 } 213 214 /** 215 * Perform any actions that need to occur before we write such as connecting to an FTP server etc. 216 */ 217 public void preWriteCheck() throws Exception { 218 // nothing needed to check 219 } 220 221 /** 222 * Perform any actions that need to occur after we are done such as disconnecting. 223 */ 224 public void postWriteCheck() { 225 // nothing needed to check 226 } 227 228 public void writeFile(Exchange exchange, String fileName) throws GenericFileOperationFailedException { 229 // build directory if auto create is enabled 230 if (endpoint.isAutoCreate()) { 231 // we must normalize it (to avoid having both \ and / in the name which confuses java.io.File) 232 String name = FileUtil.normalizePath(fileName); 233 234 // use java.io.File to compute the file path 235 File file = new File(name); 236 String directory = file.getParent(); 237 boolean absolute = FileUtil.isAbsolute(file); 238 if (directory != null) { 239 if (!operations.buildDirectory(directory, absolute)) { 240 log.debug("Cannot build directory [{}] (could be because of denied permissions)", directory); 241 } 242 } 243 } 244 245 // upload 246 if (log.isTraceEnabled()) { 247 log.trace("About to write [{}] to [{}] from exchange [{}]", new Object[]{fileName, getEndpoint(), exchange}); 248 } 249 250 boolean success = operations.storeFile(fileName, exchange); 251 if (!success) { 252 throw new GenericFileOperationFailedException("Error writing file [" + fileName + "]"); 253 } 254 log.debug("Wrote [{}] to [{}]", fileName, getEndpoint()); 255 } 256 257 public String createFileName(Exchange exchange) { 258 String answer; 259 260 String name = exchange.getIn().getHeader(Exchange.FILE_NAME, String.class); 261 262 // expression support 263 Expression expression = endpoint.getFileName(); 264 if (name != null) { 265 // the header name can be an expression too, that should override 266 // whatever configured on the endpoint 267 if (StringHelper.hasStartToken(name, "simple")) { 268 log.trace("{} contains a Simple expression: {}", Exchange.FILE_NAME, name); 269 Language language = getEndpoint().getCamelContext().resolveLanguage("file"); 270 expression = language.createExpression(name); 271 } 272 } 273 if (expression != null) { 274 log.trace("Filename evaluated as expression: {}", expression); 275 name = expression.evaluate(exchange, String.class); 276 } 277 278 // flatten name 279 if (name != null && endpoint.isFlatten()) { 280 // check for both windows and unix separators 281 int pos = Math.max(name.lastIndexOf("/"), name.lastIndexOf("\\")); 282 if (pos != -1) { 283 name = name.substring(pos + 1); 284 } 285 } 286 287 // compute path by adding endpoint starting directory 288 String endpointPath = endpoint.getConfiguration().getDirectory(); 289 String baseDir = ""; 290 if (endpointPath.length() > 0) { 291 // Its a directory so we should use it as a base path for the filename 292 // If the path isn't empty, we need to add a trailing / if it isn't already there 293 baseDir = endpointPath; 294 boolean trailingSlash = endpointPath.endsWith("/") || endpointPath.endsWith("\\"); 295 if (!trailingSlash) { 296 baseDir += getFileSeparator(); 297 } 298 } 299 if (name != null) { 300 answer = baseDir + name; 301 } else { 302 // use a generated filename if no name provided 303 answer = baseDir + endpoint.getGeneratedFileName(exchange.getIn()); 304 } 305 306 if (endpoint.getConfiguration().needToNormalize()) { 307 // must normalize path to cater for Windows and other OS 308 answer = normalizePath(answer); 309 } 310 311 return answer; 312 } 313 314 public String createTempFileName(Exchange exchange, String fileName) { 315 String answer = fileName; 316 317 String tempName; 318 if (exchange.getIn().getHeader(Exchange.FILE_NAME) == null) { 319 // its a generated filename then add it to header so we can evaluate the expression 320 exchange.getIn().setHeader(Exchange.FILE_NAME, FileUtil.stripPath(fileName)); 321 tempName = endpoint.getTempFileName().evaluate(exchange, String.class); 322 // and remove it again after evaluation 323 exchange.getIn().removeHeader(Exchange.FILE_NAME); 324 } else { 325 tempName = endpoint.getTempFileName().evaluate(exchange, String.class); 326 } 327 328 // check for both windows and unix separators 329 int pos = Math.max(answer.lastIndexOf("/"), answer.lastIndexOf("\\")); 330 if (pos == -1) { 331 // no path so use temp name as calculated 332 answer = tempName; 333 } else { 334 // path should be prefixed before the temp name 335 StringBuilder sb = new StringBuilder(answer.substring(0, pos + 1)); 336 sb.append(tempName); 337 answer = sb.toString(); 338 } 339 340 if (endpoint.getConfiguration().needToNormalize()) { 341 // must normalize path to cater for Windows and other OS 342 answer = normalizePath(answer); 343 } 344 345 return answer; 346 } 347 348 @Override 349 protected void doStart() throws Exception { 350 super.doStart(); 351 ServiceHelper.startService(locks); 352 } 353 354 @Override 355 protected void doStop() throws Exception { 356 ServiceHelper.stopService(locks); 357 super.doStop(); 358 } 359 }