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 */
017package org.apache.camel.component.file.strategy;
018
019import java.io.File;
020import java.util.regex.Pattern;
021
022import org.apache.camel.Exchange;
023import org.apache.camel.LoggingLevel;
024import org.apache.camel.component.file.FileComponent;
025import org.apache.camel.component.file.GenericFile;
026import org.apache.camel.component.file.GenericFileEndpoint;
027import org.apache.camel.component.file.GenericFileExclusiveReadLockStrategy;
028import org.apache.camel.component.file.GenericFileFilter;
029import org.apache.camel.component.file.GenericFileOperations;
030import org.apache.camel.util.FileUtil;
031import org.apache.camel.util.StopWatch;
032import org.apache.camel.util.StringHelper;
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035
036/**
037 * Acquires read lock to the given file using a marker file so other Camel consumers wont acquire the same file.
038 * This is the default behavior in Camel 1.x.
039 */
040public class MarkerFileExclusiveReadLockStrategy implements GenericFileExclusiveReadLockStrategy<File> {
041    private static final Logger LOG = LoggerFactory.getLogger(MarkerFileExclusiveReadLockStrategy.class);
042
043    private boolean markerFile = true;
044    private boolean deleteOrphanLockFiles = true;
045
046    @Override
047    public void prepareOnStartup(GenericFileOperations<File> operations, GenericFileEndpoint<File> endpoint) {
048        if (deleteOrphanLockFiles) {
049
050            String dir = endpoint.getConfiguration().getDirectory();
051            File file = new File(dir);
052
053            LOG.debug("Prepare on startup by deleting orphaned lock files from: {}", dir);
054
055            Pattern excludePattern = endpoint.getExcludePattern();
056            Pattern includePattern = endpoint.getIncludePattern();
057            String endpointPath = endpoint.getConfiguration().getDirectory();
058
059            StopWatch watch = new StopWatch();
060            deleteLockFiles(file, endpoint.isRecursive(), endpointPath, endpoint.getFilter(), endpoint.getAntFilter(), excludePattern, includePattern);
061
062            // log anything that takes more than a second
063            if (watch.taken() > 1000) {
064                LOG.info("Prepared on startup by deleting orphaned lock files from: {} took {} millis to complete.", dir, watch.taken());
065            }
066        }
067    }
068
069    @Override
070    public boolean acquireExclusiveReadLock(GenericFileOperations<File> operations,
071                                            GenericFile<File> file, Exchange exchange) throws Exception {
072
073        if (!markerFile) {
074            // if not using marker file then we assume acquired
075            return true;
076        }
077
078        String lockFileName = getLockFileName(file);
079        LOG.trace("Locking the file: {} using the lock file name: {}", file, lockFileName);
080
081        // create a plain file as marker filer for locking (do not use FileLock)
082        boolean acquired = FileUtil.createNewFile(new File(lockFileName));
083
084        // store read-lock state
085        exchange.setProperty(asReadLockKey(file, Exchange.FILE_LOCK_FILE_ACQUIRED), acquired);
086        exchange.setProperty(asReadLockKey(file, Exchange.FILE_LOCK_FILE_NAME), lockFileName);
087
088        return acquired;
089    }
090
091    @Override
092    public void releaseExclusiveReadLockOnAbort(GenericFileOperations<File> operations, GenericFile<File> file, Exchange exchange) throws Exception {
093        doReleaseExclusiveReadLock(operations, file, exchange);
094    }
095
096    @Override
097    public void releaseExclusiveReadLockOnRollback(GenericFileOperations<File> operations, GenericFile<File> file, Exchange exchange) throws Exception {
098        doReleaseExclusiveReadLock(operations, file, exchange);
099    }
100
101    @Override
102    public void releaseExclusiveReadLockOnCommit(GenericFileOperations<File> operations, GenericFile<File> file, Exchange exchange) throws Exception {
103        doReleaseExclusiveReadLock(operations, file, exchange);
104    }
105
106    protected void doReleaseExclusiveReadLock(GenericFileOperations<File> operations,
107                                              GenericFile<File> file, Exchange exchange) throws Exception {
108        if (!markerFile) {
109            // if not using marker file then nothing to release
110            return;
111        }
112
113        boolean acquired = exchange.getProperty(asReadLockKey(file, Exchange.FILE_LOCK_FILE_ACQUIRED), false, Boolean.class);
114
115        // only release the file if camel get the lock before
116        if (acquired) {
117            String lockFileName = exchange.getProperty(asReadLockKey(file, Exchange.FILE_LOCK_FILE_NAME), String.class);
118            File lock = new File(lockFileName);
119
120            if (lock.exists()) {
121                LOG.trace("Unlocking file: {}", lockFileName);
122                boolean deleted = FileUtil.deleteFile(lock);
123                LOG.trace("Lock file: {} was deleted: {}", lockFileName, deleted);
124            }
125        }
126    }
127
128    @Override
129    public void setTimeout(long timeout) {
130        // noop
131    }
132
133    @Override
134    public void setCheckInterval(long checkInterval) {
135        // noop
136    }
137
138    @Override
139    public void setReadLockLoggingLevel(LoggingLevel readLockLoggingLevel) {
140        // noop
141    }
142
143    @Override
144    public void setMarkerFiler(boolean markerFile) {
145        this.markerFile = markerFile;
146    }
147
148    @Override
149    public void setDeleteOrphanLockFiles(boolean deleteOrphanLockFiles) {
150        this.deleteOrphanLockFiles = deleteOrphanLockFiles;
151    }
152
153    private static void deleteLockFiles(File dir, boolean recursive, String endpointPath,
154                                        GenericFileFilter filter, GenericFileFilter antFilter,
155                                        Pattern excludePattern, Pattern includePattern) {
156        File[] files = dir.listFiles();
157        if (files == null || files.length == 0) {
158            return;
159        }
160
161        for (File file : files) {
162
163            if (file.getName().startsWith(".")) {
164                // files starting with dot should be skipped
165                continue;
166            }
167
168            // filter unwanted files and directories to avoid traveling everything
169            if (filter != null || antFilter != null || excludePattern != null || includePattern != null) {
170
171                File targetFile = file;
172
173                // if its a lock file then check if we accept its target file to know if we should delete the orphan lock file
174                if (file.getName().endsWith(FileComponent.DEFAULT_LOCK_FILE_POSTFIX)) {
175                    String target = file.getName().substring(0, file.getName().length() - FileComponent.DEFAULT_LOCK_FILE_POSTFIX.length());
176                    if (file.getParent() != null) {
177                        targetFile = new File(file.getParent(), target);
178                    } else {
179                        targetFile = new File(target);
180                    }
181                }
182
183                boolean accept = acceptFile(targetFile, endpointPath, filter, antFilter, excludePattern, includePattern);
184                if (!accept) {
185                    continue;
186                }
187            }
188
189            if (file.getName().endsWith(FileComponent.DEFAULT_LOCK_FILE_POSTFIX)) {
190                LOG.warn("Deleting orphaned lock file: {}", file);
191                FileUtil.deleteFile(file);
192            } else if (recursive && file.isDirectory()) {
193                deleteLockFiles(file, true, endpointPath, filter, antFilter, excludePattern, includePattern);
194            }
195        }
196    }
197
198    @SuppressWarnings("unchecked")
199    private static boolean acceptFile(File file, String endpointPath, GenericFileFilter filter, GenericFileFilter antFilter,
200                                      Pattern excludePattern, Pattern includePattern) {
201        GenericFile gf = new GenericFile();
202        gf.setEndpointPath(endpointPath);
203        gf.setFile(file);
204        gf.setFileNameOnly(file.getName());
205        gf.setFileLength(file.length());
206        gf.setDirectory(file.isDirectory());
207        // must use FileUtil.isAbsolute to have consistent check for whether the file is
208        // absolute or not. As windows do not consider \ paths as absolute where as all
209        // other OS platforms will consider \ as absolute. The logic in Camel mandates
210        // that we align this for all OS. That is why we must use FileUtil.isAbsolute
211        // to return a consistent answer for all OS platforms.
212        gf.setAbsolute(FileUtil.isAbsolute(file));
213        gf.setAbsoluteFilePath(file.getAbsolutePath());
214        gf.setLastModified(file.lastModified());
215
216        // compute the file path as relative to the starting directory
217        File path;
218        String endpointNormalized = FileUtil.normalizePath(endpointPath);
219        if (file.getPath().startsWith(endpointNormalized + File.separator)) {
220            // skip duplicate endpoint path
221            path = new File(StringHelper.after(file.getPath(), endpointNormalized + File.separator));
222        } else {
223            path = new File(file.getPath());
224        }
225
226        if (path.getParent() != null) {
227            gf.setRelativeFilePath(path.getParent() + File.separator + file.getName());
228        } else {
229            gf.setRelativeFilePath(path.getName());
230        }
231
232        // the file name should be the relative path
233        gf.setFileName(gf.getRelativeFilePath());
234
235        if (filter != null) {
236            // a custom filter can also filter directories
237            if (!filter.accept(gf)) {
238                return false;
239            }
240        }
241
242        // the following filters only works on files so allow any directory from this point
243        if (file.isDirectory()) {
244            return true;
245        }
246
247        if (antFilter != null) {
248            if (!antFilter.accept(gf)) {
249                return false;
250            }
251        }
252
253        // exclude take precedence over include
254        if (excludePattern != null)  {
255            if (excludePattern.matcher(file.getName()).matches()) {
256                return false;
257            }
258        }
259        if (includePattern != null)  {
260            if (!includePattern.matcher(file.getName()).matches()) {
261                return false;
262            }
263        }
264
265        return true;
266    }
267
268    private static String getLockFileName(GenericFile<File> file) {
269        return file.getAbsoluteFilePath() + FileComponent.DEFAULT_LOCK_FILE_POSTFIX;
270    }
271
272    private static String asReadLockKey(GenericFile file, String key) {
273        // use the copy from absolute path as that was the original path of the file when the lock was acquired
274        // for example if the file consumer uses preMove then the file is moved and therefore has another name
275        // that would no longer match
276        String path = file.getCopyFromAbsoluteFilePath() != null ? file.getCopyFromAbsoluteFilePath() : file.getAbsoluteFilePath();
277        return path + "-" + key;
278    }
279
280}