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