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