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 transient 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 public boolean storeFile(String fileName, Exchange exchange) throws GenericFileOperationFailedException { 160 ObjectHelper.notNull(endpoint, "endpoint"); 161 162 File file = new File(fileName); 163 164 // if an existing file already exists what should we do? 165 if (file.exists()) { 166 if (endpoint.getFileExist() == GenericFileExist.Ignore) { 167 // ignore but indicate that the file was written 168 LOG.trace("An existing file already exists: {}. Ignore and do not override it.", file); 169 return true; 170 } else if (endpoint.getFileExist() == GenericFileExist.Fail) { 171 throw new GenericFileOperationFailedException("File already exist: " + file + ". Cannot write new file."); 172 } else if (endpoint.getFileExist() == GenericFileExist.Move) { 173 // move any existing file first 174 doMoveExistingFile(fileName); 175 } 176 } 177 178 // Do an explicit test for a null body and decide what to do 179 if (exchange.getIn().getBody() == null) { 180 if (endpoint.isAllowNullBody()) { 181 LOG.trace("Writing empty file."); 182 try { 183 writeFileEmptyBody(file); 184 return true; 185 } catch (IOException e) { 186 throw new GenericFileOperationFailedException("Cannot store file: " + file, e); 187 } 188 } else { 189 throw new GenericFileOperationFailedException("Cannot write null body to file: " + file); 190 } 191 } 192 193 // we can write the file by 3 different techniques 194 // 1. write file to file 195 // 2. rename a file from a local work path 196 // 3. write stream to file 197 try { 198 199 // is there an explicit charset configured we must write the file as 200 String charset = endpoint.getCharset(); 201 202 // we can optimize and use file based if no charset must be used, and the input body is a file 203 File source = null; 204 boolean fileBased = false; 205 if (charset == null) { 206 // if no charset, then we can try using file directly (optimized) 207 Object body = exchange.getIn().getBody(); 208 if (body instanceof WrappedFile) { 209 body = ((WrappedFile<?>) body).getFile(); 210 fileBased = true; 211 } 212 if (body instanceof File) { 213 source = (File) body; 214 } 215 } 216 217 if (fileBased) { 218 // okay we know the body is a file based 219 220 // so try to see if we can optimize by renaming the local work path file instead of doing 221 // a full file to file copy, as the local work copy is to be deleted afterwards anyway 222 // local work path 223 File local = exchange.getIn().getHeader(Exchange.FILE_LOCAL_WORK_PATH, File.class); 224 if (local != null && local.exists()) { 225 boolean renamed = writeFileByLocalWorkPath(local, file); 226 if (renamed) { 227 // try to keep last modified timestamp if configured to do so 228 keepLastModified(exchange, file); 229 // clear header as we have renamed the file 230 exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, null); 231 // return as the operation is complete, we just renamed the local work file 232 // to the target. 233 return true; 234 } 235 } else if (source != null && source.exists()) { 236 // no there is no local work file so use file to file copy if the source exists 237 writeFileByFile(source, file); 238 // try to keep last modified timestamp if configured to do so 239 keepLastModified(exchange, file); 240 return true; 241 } 242 } 243 244 if (charset != null) { 245 // charset configured so we must use a reader so we can write with encoding 246 Reader in = exchange.getIn().getBody(Reader.class); 247 if (in == null) { 248 // okay no direct reader conversion, so use an input stream (which a lot can be converted as) 249 InputStream is = exchange.getIn().getMandatoryBody(InputStream.class); 250 in = new InputStreamReader(is); 251 } 252 // buffer the reader 253 in = IOHelper.buffered(in); 254 writeFileByReaderWithCharset(in, file, charset); 255 } else { 256 // fallback and use stream based 257 InputStream in = exchange.getIn().getMandatoryBody(InputStream.class); 258 writeFileByStream(in, file); 259 } 260 // try to keep last modified timestamp if configured to do so 261 keepLastModified(exchange, file); 262 return true; 263 } catch (IOException e) { 264 throw new GenericFileOperationFailedException("Cannot store file: " + file, e); 265 } catch (InvalidPayloadException e) { 266 throw new GenericFileOperationFailedException("Cannot store file: " + file, e); 267 } 268 } 269 270 /** 271 * Moves any existing file due fileExists=Move is in use. 272 */ 273 private void doMoveExistingFile(String fileName) throws GenericFileOperationFailedException { 274 // need to evaluate using a dummy and simulate the file first, to have access to all the file attributes 275 // create a dummy exchange as Exchange is needed for expression evaluation 276 // we support only the following 3 tokens. 277 Exchange dummy = endpoint.createExchange(); 278 String parent = FileUtil.onlyPath(fileName); 279 String onlyName = FileUtil.stripPath(fileName); 280 dummy.getIn().setHeader(Exchange.FILE_NAME, fileName); 281 dummy.getIn().setHeader(Exchange.FILE_NAME_ONLY, onlyName); 282 dummy.getIn().setHeader(Exchange.FILE_PARENT, parent); 283 284 String to = endpoint.getMoveExisting().evaluate(dummy, String.class); 285 // we must normalize it (to avoid having both \ and / in the name which confuses java.io.File) 286 to = FileUtil.normalizePath(to); 287 if (ObjectHelper.isEmpty(to)) { 288 throw new GenericFileOperationFailedException("moveExisting evaluated as empty String, cannot move existing file: " + fileName); 289 } 290 291 // ensure any paths is created before we rename as the renamed file may be in a different path (which may be non exiting) 292 // use java.io.File to compute the file path 293 File toFile = new File(to); 294 String directory = toFile.getParent(); 295 boolean absolute = FileUtil.isAbsolute(toFile); 296 if (directory != null) { 297 if (!buildDirectory(directory, absolute)) { 298 LOG.debug("Cannot build directory [{}] (could be because of denied permissions)", directory); 299 } 300 } 301 302 // deal if there already exists a file 303 if (existsFile(to)) { 304 if (endpoint.isEagerDeleteTargetFile()) { 305 LOG.trace("Deleting existing file: {}", to); 306 if (!deleteFile(to)) { 307 throw new GenericFileOperationFailedException("Cannot delete file: " + to); 308 } 309 } else { 310 throw new GenericFileOperationFailedException("Cannot moved existing file from: " + fileName + " to: " + to + " as there already exists a file: " + to); 311 } 312 } 313 314 LOG.trace("Moving existing file: {} to: {}", fileName, to); 315 if (!renameFile(fileName, to)) { 316 throw new GenericFileOperationFailedException("Cannot rename file from: " + fileName + " to: " + to); 317 } 318 } 319 320 private void keepLastModified(Exchange exchange, File file) { 321 if (endpoint.isKeepLastModified()) { 322 Long last; 323 Date date = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Date.class); 324 if (date != null) { 325 last = date.getTime(); 326 } else { 327 // fallback and try a long 328 last = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Long.class); 329 } 330 if (last != null) { 331 boolean result = file.setLastModified(last); 332 if (LOG.isTraceEnabled()) { 333 LOG.trace("Keeping last modified timestamp: {} on file: {} with result: {}", new Object[]{last, file, result}); 334 } 335 } 336 } 337 } 338 339 private boolean writeFileByLocalWorkPath(File source, File file) throws IOException { 340 LOG.trace("Using local work file being renamed from: {} to: {}", source, file); 341 return FileUtil.renameFile(source, file, endpoint.isCopyAndDeleteOnRenameFail()); 342 } 343 344 private void writeFileByFile(File source, File target) throws IOException { 345 FileChannel in = new FileInputStream(source).getChannel(); 346 FileChannel out = null; 347 try { 348 out = prepareOutputFileChannel(target); 349 LOG.debug("Using FileChannel to write file: {}", target); 350 long size = in.size(); 351 long position = 0; 352 while (position < size) { 353 position += in.transferTo(position, endpoint.getBufferSize(), out); 354 } 355 } finally { 356 IOHelper.close(in, source.getName(), LOG); 357 IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites()); 358 } 359 } 360 361 private void writeFileByStream(InputStream in, File target) throws IOException { 362 FileChannel out = null; 363 try { 364 out = prepareOutputFileChannel(target); 365 LOG.debug("Using InputStream to write file: {}", target); 366 int size = endpoint.getBufferSize(); 367 byte[] buffer = new byte[size]; 368 ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); 369 int bytesRead; 370 while ((bytesRead = in.read(buffer)) != -1) { 371 if (bytesRead < size) { 372 byteBuffer.limit(bytesRead); 373 } 374 out.write(byteBuffer); 375 byteBuffer.clear(); 376 } 377 } finally { 378 IOHelper.close(in, target.getName(), LOG); 379 IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites()); 380 } 381 } 382 383 private void writeFileByReaderWithCharset(Reader in, File target, String charset) throws IOException { 384 boolean append = endpoint.getFileExist() == GenericFileExist.Append; 385 FileOutputStream os = new FileOutputStream(target, append); 386 Writer out = IOConverter.toWriter(os, charset); 387 try { 388 LOG.debug("Using Reader to write file: {} with charset: {}", target, charset); 389 int size = endpoint.getBufferSize(); 390 IOHelper.copy(in, out, size); 391 } finally { 392 IOHelper.close(in, target.getName(), LOG); 393 IOHelper.close(out, os, target.getName(), LOG, endpoint.isForceWrites()); 394 } 395 } 396 397 /** 398 * Creates a new file if the file doesn't exist. 399 * If the endpoint's existing file logic is set to 'Override' then the target file will be truncated 400 */ 401 private void writeFileEmptyBody(File target) throws IOException { 402 if (!target.exists()) { 403 LOG.debug("Creating new empty file: {}", target); 404 FileUtil.createNewFile(target); 405 } else if (endpoint.getFileExist() == GenericFileExist.Override) { 406 LOG.debug("Truncating existing file: {}", target); 407 FileChannel out = new FileOutputStream(target).getChannel(); 408 try { 409 out.truncate(0); 410 } finally { 411 IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites()); 412 } 413 } 414 } 415 416 /** 417 * Creates and prepares the output file channel. Will position itself in correct position if the file is writable 418 * eg. it should append or override any existing content. 419 */ 420 private FileChannel prepareOutputFileChannel(File target) throws IOException { 421 if (endpoint.getFileExist() == GenericFileExist.Append) { 422 FileChannel out = new RandomAccessFile(target, "rw").getChannel(); 423 return out.position(out.size()); 424 } 425 return new FileOutputStream(target).getChannel(); 426 } 427 }