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