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     * @param path
093     * @since 7.2
094     */
095    public static void registerProtectedPath(String path) {
096        deleteStrategy.registerProtectedPath(path);
097    }
098
099    protected class GCDelegate implements FileEventHandler {
100        protected FileCleaningTracker delegate = new FileCleaningTracker();
101
102        @Override
103        public void onFile(File file, Object marker) {
104            delegate.track(file, marker, deleteStrategy);
105        }
106    }
107
108    protected class ThreadDelegate implements FileEventHandler {
109
110        protected final boolean isLongRunning;
111
112        protected final Thread owner = Thread.currentThread();
113
114        protected final Set<File> files = new HashSet<>();
115
116        protected ThreadDelegate(boolean isLongRunning) {
117            this.isLongRunning = isLongRunning;
118        }
119
120        @Override
121        public void onFile(File file, Object marker) {
122            if (!owner.equals(Thread.currentThread())) {
123                return;
124            }
125            if (isLongRunning) {
126                gc.onFile(file, marker);
127            }
128            files.add(file);
129        }
130
131    }
132
133    @XObject("enableThreadsTracking")
134    public static class EnableThreadsTracking {
135
136    }
137
138    protected final GCDelegate gc = new GCDelegate();
139
140    protected static FileEventTracker self;
141
142    protected final ThreadLocal<ThreadDelegate> threads = new ThreadLocal<>();
143
144    protected final ThreadEventListener threadsListener = new ThreadEventListener(new ThreadEventHandler() {
145
146        @Override
147        public void onEnter(boolean isLongRunning) {
148            setThreadDelegate(isLongRunning);
149        }
150
151        @Override
152        public void onLeave() {
153            resetThreadDelegate();
154        }
155
156    });
157
158    protected final FileEventListener filesListener = new FileEventListener(new FileEventHandler() {
159
160        @Override
161        public void onFile(File file, Object marker) {
162            onContext().onFile(file, marker);
163        }
164    });
165
166    @Override
167    public void activate(ComponentContext context) {
168        super.activate(context);
169        self = this;
170        filesListener.install();
171        setThreadDelegate(false);
172    }
173
174    @Override
175    public int getApplicationStartedOrder() {
176        return Integer.MAX_VALUE;
177    }
178
179    @Override
180    public void start(ComponentContext context) {
181        resetThreadDelegate();
182    }
183
184    @Override
185    public void deactivate(ComponentContext context) {
186        if (Framework.getService(EventService.class) != null) {
187            if (threadsListener.isInstalled()) {
188                threadsListener.uninstall();
189            }
190            filesListener.uninstall();
191        }
192        self = null;
193        super.deactivate(context);
194    }
195
196    @Override
197    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
198        if (contribution instanceof EnableThreadsTracking) {
199            threadsListener.install();
200        } else {
201            super.registerContribution(contribution, extensionPoint, contributor);
202        }
203
204    }
205
206    protected FileEventHandler onContext() {
207        FileEventHandler actual = threads.get();
208        if (actual == null) {
209            actual = gc;
210        }
211        return actual;
212    }
213
214    protected void setThreadDelegate(boolean isLongRunning) {
215        if (threads.get() != null) {
216            throw new IllegalStateException("Thread delegate already installed");
217        }
218        threads.set(new ThreadDelegate(isLongRunning));
219    }
220
221    protected void resetThreadDelegate() throws IllegalStateException {
222        ThreadDelegate actual = threads.get();
223        if (actual == null) {
224            return;
225        }
226        try {
227            for (File file : actual.files) {
228                if (!deleteStrategy.isFileProtected(file)) {
229                    file.delete();
230                }
231            }
232        } finally {
233            threads.remove();
234        }
235    }
236
237}