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.io.FileInputStream; 021 import java.io.FileOutputStream; 022 import java.io.IOException; 023 import java.io.InputStream; 024 import java.io.InputStreamReader; 025 import java.io.RandomAccessFile; 026 import java.io.Reader; 027 import java.io.Writer; 028 import java.nio.ByteBuffer; 029 import java.nio.channels.FileChannel; 030 import java.util.Date; 031 import java.util.List; 032 033 import org.apache.camel.Exchange; 034 import org.apache.camel.InvalidPayloadException; 035 import org.apache.camel.WrappedFile; 036 import org.apache.camel.converter.IOConverter; 037 import org.apache.camel.util.FileUtil; 038 import org.apache.camel.util.IOHelper; 039 import org.apache.camel.util.ObjectHelper; 040 import org.slf4j.Logger; 041 import org.slf4j.LoggerFactory; 042 043 /** 044 * File operations for {@link java.io.File}. 045 */ 046 public class FileOperations implements GenericFileOperations<File> { 047 private static final Logger LOG = LoggerFactory.getLogger(FileOperations.class); 048 private FileEndpoint endpoint; 049 050 public FileOperations() { 051 } 052 053 public FileOperations(FileEndpoint endpoint) { 054 this.endpoint = endpoint; 055 } 056 057 public void setEndpoint(GenericFileEndpoint<File> endpoint) { 058 this.endpoint = (FileEndpoint) endpoint; 059 } 060 061 public boolean deleteFile(String name) throws GenericFileOperationFailedException { 062 File file = new File(name); 063 return FileUtil.deleteFile(file); 064 } 065 066 public boolean renameFile(String from, String to) throws GenericFileOperationFailedException { 067 File file = new File(from); 068 File target = new File(to); 069 try { 070 return FileUtil.renameFile(file, target, endpoint.isCopyAndDeleteOnRenameFail()); 071 } catch (IOException e) { 072 throw new GenericFileOperationFailedException("Error renaming file from " + from + " to " + to, e); 073 } 074 } 075 076 public boolean existsFile(String name) throws GenericFileOperationFailedException { 077 File file = new File(name); 078 return file.exists(); 079 } 080 081 public boolean buildDirectory(String directory, boolean absolute) throws GenericFileOperationFailedException { 082 ObjectHelper.notNull(endpoint, "endpoint"); 083 084 // always create endpoint defined directory 085 if (endpoint.isAutoCreate() && !endpoint.getFile().exists()) { 086 LOG.trace("Building starting directory: {}", endpoint.getFile()); 087 endpoint.getFile().mkdirs(); 088 } 089 090 if (ObjectHelper.isEmpty(directory)) { 091 // no directory to build so return true to indicate ok 092 return true; 093 } 094 095 File endpointPath = endpoint.getFile(); 096 File target = new File(directory); 097 098 File path; 099 if (absolute) { 100 // absolute path 101 path = target; 102 } else if (endpointPath.equals(target)) { 103 // its just the root of the endpoint path 104 path = endpointPath; 105 } else { 106 // relative after the endpoint path 107 String afterRoot = ObjectHelper.after(directory, endpointPath.getPath() + File.separator); 108 if (ObjectHelper.isNotEmpty(afterRoot)) { 109 // dir is under the root path 110 path = new File(endpoint.getFile(), afterRoot); 111 } else { 112 // dir is relative to the root path 113 path = new File(endpoint.getFile(), directory); 114 } 115 } 116 117 // We need to make sure that this is thread-safe and only one thread tries to create the path directory at the same time. 118 synchronized (this) { 119 if (path.isDirectory() && path.exists()) { 120 // the directory already exists 121 return true; 122 } else { 123 if (LOG.isTraceEnabled()) { 124 LOG.trace("Building directory: {}", path); 125 } 126 return path.mkdirs(); 127 } 128 } 129 } 130 131 public List<File> listFiles() throws GenericFileOperationFailedException { 132 // noop 133 return null; 134 } 135 136 public List<File> listFiles(String path) throws GenericFileOperationFailedException { 137 // noop 138 return null; 139 } 140 141 public void changeCurrentDirectory(String path) throws GenericFileOperationFailedException { 142 // noop 143 } 144 145 public void changeToParentDirectory() throws GenericFileOperationFailedException { 146 // noop 147 } 148 149 public String getCurrentDirectory() throws GenericFileOperationFailedException { 150 // noop 151 return null; 152 } 153 154 public boolean retrieveFile(String name, Exchange exchange) throws GenericFileOperationFailedException { 155 // noop as we use type converters to read the body content for java.io.File 156 return true; 157 } 158 159 @Override 160 public void releaseRetreivedFileResources(Exchange exchange) throws GenericFileOperationFailedException { 161 // noop as we used type converters to read the body content for java.io.File 162 } 163 164 public boolean storeFile(String fileName, Exchange exchange) throws GenericFileOperationFailedException { 165 ObjectHelper.notNull(endpoint, "endpoint"); 166 167 File file = new File(fileName); 168 169 // if an existing file already exists what should we do? 170 if (file.exists()) { 171 if (endpoint.getFileExist() == GenericFileExist.Ignore) { 172 // ignore but indicate that the file was written 173 LOG.trace("An existing file already exists: {}. Ignore and do not override it.", file); 174 return true; 175 } else if (endpoint.getFileExist() == GenericFileExist.Fail) { 176 throw new GenericFileOperationFailedException("File already exist: " + file + ". Cannot write new file."); 177 } else if (endpoint.getFileExist() == GenericFileExist.Move) { 178 // move any existing file first 179 doMoveExistingFile(fileName); 180 } 181 } 182 183 // Do an explicit test for a null body and decide what to do 184 if (exchange.getIn().getBody() == null) { 185 if (endpoint.isAllowNullBody()) { 186 LOG.trace("Writing empty file."); 187 try { 188 writeFileEmptyBody(file); 189 return true; 190 } catch (IOException e) { 191 throw new GenericFileOperationFailedException("Cannot store file: " + file, e); 192 } 193 } else { 194 throw new GenericFileOperationFailedException("Cannot write null body to file: " + file); 195 } 196 } 197 198 // we can write the file by 3 different techniques 199 // 1. write file to file 200 // 2. rename a file from a local work path 201 // 3. write stream to file 202 try { 203 204 // is there an explicit charset configured we must write the file as 205 String charset = endpoint.getCharset(); 206 207 // we can optimize and use file based if no charset must be used, and the input body is a file 208 File source = null; 209 boolean fileBased = false; 210 if (charset == null) { 211 // if no charset, then we can try using file directly (optimized) 212 Object body = exchange.getIn().getBody(); 213 if (body instanceof WrappedFile) { 214 body = ((WrappedFile<?>) body).getFile(); 215 fileBased = true; 216 } 217 if (body instanceof File) { 218 source = (File) body; 219 } 220 } 221 222 if (fileBased) { 223 // okay we know the body is a file based 224 225 // so try to see if we can optimize by renaming the local work path file instead of doing 226 // a full file to file copy, as the local work copy is to be deleted afterwards anyway 227 // local work path 228 File local = exchange.getIn().getHeader(Exchange.FILE_LOCAL_WORK_PATH, File.class); 229 if (local != null && local.exists()) { 230 boolean renamed = writeFileByLocalWorkPath(local, file); 231 if (renamed) { 232 // try to keep last modified timestamp if configured to do so 233 keepLastModified(exchange, file); 234 // clear header as we have renamed the file 235 exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, null); 236 // return as the operation is complete, we just renamed the local work file 237 // to the target. 238 return true; 239 } 240 } else if (source != null && source.exists()) { 241 // no there is no local work file so use file to file copy if the source exists 242 writeFileByFile(source, file); 243 // try to keep last modified timestamp if configured to do so 244 keepLastModified(exchange, file); 245 return true; 246 } 247 } 248 249 if (charset != null) { 250 // charset configured so we must use a reader so we can write with encoding 251 Reader in = exchange.getIn().getBody(Reader.class); 252 if (in == null) { 253 // okay no direct reader conversion, so use an input stream (which a lot can be converted as) 254 InputStream is = exchange.getIn().getMandatoryBody(InputStream.class); 255 in = new InputStreamReader(is); 256 } 257 // buffer the reader 258 in = IOHelper.buffered(in); 259 writeFileByReaderWithCharset(in, file, charset); 260 } else { 261 // fallback and use stream based 262 InputStream in = exchange.getIn().getMandatoryBody(InputStream.class); 263 writeFileByStream(in, file); 264 } 265 // try to keep last modified timestamp if configured to do so 266 keepLastModified(exchange, file); 267 return true; 268 } catch (IOException e) { 269 throw new GenericFileOperationFailedException("Cannot store file: " + file, e); 270 } catch (InvalidPayloadException e) { 271 throw new GenericFileOperationFailedException("Cannot store file: " + file, e); 272 } 273 } 274 275 /** 276 * Moves any existing file due fileExists=Move is in use. 277 */ 278 private void doMoveExistingFile(String fileName) throws GenericFileOperationFailedException { 279 // need to evaluate using a dummy and simulate the file first, to have access to all the file attributes 280 // create a dummy exchange as Exchange is needed for expression evaluation 281 // we support only the following 3 tokens. 282 Exchange dummy = endpoint.createExchange(); 283 String parent = FileUtil.onlyPath(fileName); 284 String onlyName = FileUtil.stripPath(fileName); 285 dummy.getIn().setHeader(Exchange.FILE_NAME, fileName); 286 dummy.getIn().setHeader(Exchange.FILE_NAME_ONLY, onlyName); 287 dummy.getIn().setHeader(Exchange.FILE_PARENT, parent); 288 289 String to = endpoint.getMoveExisting().evaluate(dummy, String.class); 290 // we must normalize it (to avoid having both \ and / in the name which confuses java.io.File) 291 to = FileUtil.normalizePath(to); 292 if (ObjectHelper.isEmpty(to)) { 293 throw new GenericFileOperationFailedException("moveExisting evaluated as empty String, cannot move existing file: " + fileName); 294 } 295 296 // ensure any paths is created before we rename as the renamed file may be in a different path (which may be non exiting) 297 // use java.io.File to compute the file path 298 File toFile = new File(to); 299 String directory = toFile.getParent(); 300 boolean absolute = FileUtil.isAbsolute(toFile); 301 if (directory != null) { 302 if (!buildDirectory(directory, absolute)) { 303 LOG.debug("Cannot build directory [{}] (could be because of denied permissions)", directory); 304 } 305 } 306 307 // deal if there already exists a file 308 if (existsFile(to)) { 309 if (endpoint.isEagerDeleteTargetFile()) { 310 LOG.trace("Deleting existing file: {}", to); 311 if (!deleteFile(to)) { 312 throw new GenericFileOperationFailedException("Cannot delete file: " + to); 313 } 314 } else { 315 throw new GenericFileOperationFailedException("Cannot moved existing file from: " + fileName + " to: " + to + " as there already exists a file: " + to); 316 } 317 } 318 319 LOG.trace("Moving existing file: {} to: {}", fileName, to); 320 if (!renameFile(fileName, to)) { 321 throw new GenericFileOperationFailedException("Cannot rename file from: " + fileName + " to: " + to); 322 } 323 } 324 325 private void keepLastModified(Exchange exchange, File file) { 326 if (endpoint.isKeepLastModified()) { 327 Long last; 328 Date date = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Date.class); 329 if (date != null) { 330 last = date.getTime(); 331 } else { 332 // fallback and try a long 333 last = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Long.class); 334 } 335 if (last != null) { 336 boolean result = file.setLastModified(last); 337 if (LOG.isTraceEnabled()) { 338 LOG.trace("Keeping last modified timestamp: {} on file: {} with result: {}", new Object[]{last, file, result}); 339 } 340 } 341 } 342 } 343 344 private boolean writeFileByLocalWorkPath(File source, File file) throws IOException { 345 LOG.trace("Using local work file being renamed from: {} to: {}", source, file); 346 return FileUtil.renameFile(source, file, endpoint.isCopyAndDeleteOnRenameFail()); 347 } 348 349 private void writeFileByFile(File source, File target) throws IOException { 350 FileChannel in = new FileInputStream(source).getChannel(); 351 FileChannel out = null; 352 try { 353 out = prepareOutputFileChannel(target); 354 LOG.debug("Using FileChannel to write file: {}", target); 355 long size = in.size(); 356 long position = 0; 357 while (position < size) { 358 position += in.transferTo(position, endpoint.getBufferSize(), out); 359 } 360 } finally { 361 IOHelper.close(in, source.getName(), LOG); 362 IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites()); 363 } 364 } 365 366 private void writeFileByStream(InputStream in, File target) throws IOException { 367 FileChannel out = null; 368 try { 369 out = prepareOutputFileChannel(target); 370 LOG.debug("Using InputStream to write file: {}", target); 371 int size = endpoint.getBufferSize(); 372 byte[] buffer = new byte[size]; 373 ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); 374 int bytesRead; 375 while ((bytesRead = in.read(buffer)) != -1) { 376 if (bytesRead < size) { 377 byteBuffer.limit(bytesRead); 378 } 379 out.write(byteBuffer); 380 byteBuffer.clear(); 381 } 382 } finally { 383 IOHelper.close(in, target.getName(), LOG); 384 IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites()); 385 } 386 } 387 388 private void writeFileByReaderWithCharset(Reader in, File target, String charset) throws IOException { 389 boolean append = endpoint.getFileExist() == GenericFileExist.Append; 390 FileOutputStream os = new FileOutputStream(target, append); 391 Writer out = IOConverter.toWriter(os, charset); 392 try { 393 LOG.debug("Using Reader to write file: {} with charset: {}", target, charset); 394 int size = endpoint.getBufferSize(); 395 IOHelper.copy(in, out, size); 396 } finally { 397 IOHelper.close(in, target.getName(), LOG); 398 IOHelper.close(out, os, target.getName(), LOG, endpoint.isForceWrites()); 399 } 400 } 401 402 /** 403 * Creates a new file if the file doesn't exist. 404 * If the endpoint's existing file logic is set to 'Override' then the target file will be truncated 405 */ 406 private void writeFileEmptyBody(File target) throws IOException { 407 if (!target.exists()) { 408 LOG.debug("Creating new empty file: {}", target); 409 FileUtil.createNewFile(target); 410 } else if (endpoint.getFileExist() == GenericFileExist.Override) { 411 LOG.debug("Truncating existing file: {}", target); 412 FileChannel out = new FileOutputStream(target).getChannel(); 413 try { 414 out.truncate(0); 415 } finally { 416 IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites()); 417 } 418 } 419 } 420 421 /** 422 * Creates and prepares the output file channel. Will position itself in correct position if the file is writable 423 * eg. it should append or override any existing content. 424 */ 425 private FileChannel prepareOutputFileChannel(File target) throws IOException { 426 if (endpoint.getFileExist() == GenericFileExist.Append) { 427 FileChannel out = new RandomAccessFile(target, "rw").getChannel(); 428 return out.position(out.size()); 429 } 430 return new FileOutputStream(target).getChannel(); 431 } 432 }