001/*
002 * (C) Copyright 2006-2017 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.nuxeo.runtime.trackers.files;
017
018import java.io.File;
019import java.io.IOException;
020import java.util.HashSet;
021import java.util.Set;
022import java.util.concurrent.CopyOnWriteArrayList;
023
024import org.apache.commons.io.FileCleaningTracker;
025import org.apache.commons.io.FileDeleteStrategy;
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028import org.nuxeo.common.xmap.annotation.XObject;
029import org.nuxeo.runtime.api.Framework;
030import org.nuxeo.runtime.model.ComponentContext;
031import org.nuxeo.runtime.model.ComponentInstance;
032import org.nuxeo.runtime.model.DefaultComponent;
033import org.nuxeo.runtime.services.event.EventService;
034import org.nuxeo.runtime.trackers.concurrent.ThreadEventHandler;
035import org.nuxeo.runtime.trackers.concurrent.ThreadEventListener;
036
037/**
038 * Files event tracker which delete files once the runtime leave the threads or at least once the associated marker
039 * object is garbaged. Note: for being backward compatible you may disable the thread events tracking by black-listing
040 * the default configuration component "org.nuxeo.runtime.trackers.files.threadstracking.config" in the runtime. This
041 * could be achieved by editing the "blacklist" file in your 'config' directory or using the @{link BlacklistComponent}
042 * annotation on your test class.
043 *
044 * @author Stephane Lacoin at Nuxeo (aka matic)
045 * @since 6.0
046 * @see ThreadEventHandler
047 */
048public class FileEventTracker extends DefaultComponent {
049
050    protected static final Log log = LogFactory.getLog(FileEventTracker.class);
051
052    protected static SafeFileDeleteStrategy deleteStrategy = new SafeFileDeleteStrategy();
053
054    static class SafeFileDeleteStrategy extends FileDeleteStrategy {
055
056        protected CopyOnWriteArrayList<String> protectedPaths = new CopyOnWriteArrayList<>();
057
058        protected SafeFileDeleteStrategy() {
059            super("DoNotTouchNuxeoBinaries");
060        }
061
062        protected void registerProtectedPath(String path) {
063            protectedPaths.add(path);
064        }
065
066        protected boolean isFileProtected(File fileToDelete) {
067            for (String path : protectedPaths) {
068                // do not delete files under the protected directories
069                if (fileToDelete.getPath().startsWith(path)) {
070                    log.warn("Protect file " + fileToDelete.getPath()
071                            + " from deletion : check usage of Framework.trackFile");
072                    return true;
073                }
074            }
075            return false;
076        }
077
078        @Override
079        protected boolean doDelete(File fileToDelete) throws IOException {
080            if (!isFileProtected(fileToDelete)) {
081                return super.doDelete(fileToDelete);
082            } else {
083                return false;
084            }
085        }
086
087    }
088
089    /**
090     * Registers a protected path under which files should not be deleted
091     *
092     * @since 7.2
093     */
094    public static void registerProtectedPath(String path) {
095        deleteStrategy.registerProtectedPath(path);
096    }
097
098    protected static class GCDelegate implements FileEventHandler {
099        protected FileCleaningTracker delegate = new FileCleaningTracker();
100
101        @Override
102        public void onFile(File file, Object marker) {
103            delegate.track(file, marker, deleteStrategy);
104        }
105    }
106
107    protected class ThreadDelegate implements FileEventHandler {
108
109        protected final boolean isLongRunning;
110
111        protected final Thread owner = Thread.currentThread();
112
113        protected final Set<File> files = new HashSet<>();
114
115        protected ThreadDelegate(boolean isLongRunning) {
116            this.isLongRunning = isLongRunning;
117        }
118
119        @Override
120        public void onFile(File file, Object marker) {
121            if (!owner.equals(Thread.currentThread())) {
122                return;
123            }
124            if (isLongRunning) {
125                gc.onFile(file, marker);
126            }
127            files.add(file);
128        }
129
130    }
131
132    @XObject("enableThreadsTracking")
133    public static class EnableThreadsTracking {
134
135    }
136
137    protected final GCDelegate gc = new GCDelegate();
138
139    protected final ThreadLocal<ThreadDelegate> threads = new ThreadLocal<>();
140
141    protected final ThreadEventListener threadsListener = new ThreadEventListener(new ThreadEventHandler() {
142
143        @Override
144        public void onEnter(boolean isLongRunning) {
145            setThreadDelegate(isLongRunning);
146        }
147
148        @Override
149        public void onLeave() {
150            resetThreadDelegate();
151        }
152
153    });
154
155    protected final FileEventListener filesListener = new FileEventListener(
156            (file, marker) -> onContext().onFile(file, marker));
157
158    @Override
159    public void activate(ComponentContext context) {
160        super.activate(context);
161        filesListener.install();
162        setThreadDelegate(false);
163    }
164
165    @Override
166    public int getApplicationStartedOrder() {
167        return Integer.MAX_VALUE;
168    }
169
170    @Override
171    public void start(ComponentContext context) {
172        resetThreadDelegate();
173    }
174
175    @Override
176    public void deactivate(ComponentContext context) {
177        if (Framework.getService(EventService.class) != null) {
178            if (threadsListener.isInstalled()) {
179                threadsListener.uninstall();
180            }
181            filesListener.uninstall();
182        }
183        super.deactivate(context);
184    }
185
186    @Override
187    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
188        if (contribution instanceof EnableThreadsTracking) {
189            threadsListener.install();
190        } else {
191            super.registerContribution(contribution, extensionPoint, contributor);
192        }
193
194    }
195
196    protected FileEventHandler onContext() {
197        FileEventHandler actual = threads.get();
198        if (actual == null) {
199            actual = gc;
200        }
201        return actual;
202    }
203
204    protected void setThreadDelegate(boolean isLongRunning) {
205        if (threads.get() != null) {
206            throw new IllegalStateException("Thread delegate already installed");
207        }
208        threads.set(new ThreadDelegate(isLongRunning));
209    }
210
211    protected void resetThreadDelegate() throws IllegalStateException {
212        ThreadDelegate actual = threads.get();
213        if (actual == null) {
214            return;
215        }
216        try {
217            for (File file : actual.files) {
218                if (!deleteStrategy.isFileProtected(file)) {
219                    file.delete();
220                }
221            }
222        } finally {
223            threads.remove();
224        }
225    }
226
227}