001/*
002 * Copyright 2012-2017 the original author or authors.
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 */
016
017package org.springframework.boot.devtools.restart.server;
018
019import java.io.File;
020import java.io.IOException;
021import java.net.URL;
022import java.net.URLClassLoader;
023import java.util.Collections;
024import java.util.LinkedHashSet;
025import java.util.Map.Entry;
026import java.util.Set;
027
028import org.apache.commons.logging.Log;
029import org.apache.commons.logging.LogFactory;
030
031import org.springframework.boot.devtools.restart.Restarter;
032import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile;
033import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kind;
034import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles;
035import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles.SourceFolder;
036import org.springframework.util.Assert;
037import org.springframework.util.FileCopyUtils;
038import org.springframework.util.ResourceUtils;
039
040/**
041 * Server used to {@link Restarter restart} the current application with updated
042 * {@link ClassLoaderFiles}.
043 *
044 * @author Phillip Webb
045 * @since 1.3.0
046 */
047public class RestartServer {
048
049        private static final Log logger = LogFactory.getLog(RestartServer.class);
050
051        private final SourceFolderUrlFilter sourceFolderUrlFilter;
052
053        private final ClassLoader classLoader;
054
055        /**
056         * Create a new {@link RestartServer} instance.
057         * @param sourceFolderUrlFilter the source filter used to link remote folder to the
058         * local classpath
059         */
060        public RestartServer(SourceFolderUrlFilter sourceFolderUrlFilter) {
061                this(sourceFolderUrlFilter, Thread.currentThread().getContextClassLoader());
062        }
063
064        /**
065         * Create a new {@link RestartServer} instance.
066         * @param sourceFolderUrlFilter the source filter used to link remote folder to the
067         * local classpath
068         * @param classLoader the application classloader
069         */
070        public RestartServer(SourceFolderUrlFilter sourceFolderUrlFilter,
071                        ClassLoader classLoader) {
072                Assert.notNull(sourceFolderUrlFilter, "SourceFolderUrlFilter must not be null");
073                Assert.notNull(classLoader, "ClassLoader must not be null");
074                this.sourceFolderUrlFilter = sourceFolderUrlFilter;
075                this.classLoader = classLoader;
076        }
077
078        /**
079         * Update the current running application with the specified {@link ClassLoaderFiles}
080         * and trigger a reload.
081         * @param files updated class loader files
082         */
083        public void updateAndRestart(ClassLoaderFiles files) {
084                Set<URL> urls = new LinkedHashSet<>();
085                Set<URL> classLoaderUrls = getClassLoaderUrls();
086                for (SourceFolder folder : files.getSourceFolders()) {
087                        for (Entry<String, ClassLoaderFile> entry : folder.getFilesEntrySet()) {
088                                for (URL url : classLoaderUrls) {
089                                        if (updateFileSystem(url, entry.getKey(), entry.getValue())) {
090                                                urls.add(url);
091                                        }
092                                }
093                        }
094                        urls.addAll(getMatchingUrls(classLoaderUrls, folder.getName()));
095                }
096                updateTimeStamp(urls);
097                restart(urls, files);
098        }
099
100        private boolean updateFileSystem(URL url, String name,
101                        ClassLoaderFile classLoaderFile) {
102                if (!isFolderUrl(url.toString())) {
103                        return false;
104                }
105                try {
106                        File folder = ResourceUtils.getFile(url);
107                        File file = new File(folder, name);
108                        if (file.exists() && file.canWrite()) {
109                                if (classLoaderFile.getKind() == Kind.DELETED) {
110                                        return file.delete();
111                                }
112                                FileCopyUtils.copy(classLoaderFile.getContents(), file);
113                                return true;
114                        }
115                }
116                catch (IOException ex) {
117                        // Ignore
118                }
119                return false;
120        }
121
122        private boolean isFolderUrl(String urlString) {
123                return urlString.startsWith("file:") && urlString.endsWith("/");
124        }
125
126        private Set<URL> getMatchingUrls(Set<URL> urls, String sourceFolder) {
127                Set<URL> matchingUrls = new LinkedHashSet<>();
128                for (URL url : urls) {
129                        if (this.sourceFolderUrlFilter.isMatch(sourceFolder, url)) {
130                                if (logger.isDebugEnabled()) {
131                                        logger.debug("URL " + url + " matched against source folder "
132                                                        + sourceFolder);
133                                }
134                                matchingUrls.add(url);
135                        }
136                }
137                return matchingUrls;
138        }
139
140        private Set<URL> getClassLoaderUrls() {
141                Set<URL> urls = new LinkedHashSet<>();
142                ClassLoader classLoader = this.classLoader;
143                while (classLoader != null) {
144                        if (classLoader instanceof URLClassLoader) {
145                                Collections.addAll(urls, ((URLClassLoader) classLoader).getURLs());
146                        }
147                        classLoader = classLoader.getParent();
148                }
149                return urls;
150
151        }
152
153        private void updateTimeStamp(Iterable<URL> urls) {
154                for (URL url : urls) {
155                        updateTimeStamp(url);
156                }
157        }
158
159        private void updateTimeStamp(URL url) {
160                try {
161                        URL actualUrl = ResourceUtils.extractJarFileURL(url);
162                        File file = ResourceUtils.getFile(actualUrl, "Jar URL");
163                        file.setLastModified(System.currentTimeMillis());
164                }
165                catch (Exception ex) {
166                        // Ignore
167                }
168        }
169
170        /**
171         * Called to restart the application.
172         * @param urls the updated URLs
173         * @param files the updated files
174         */
175        protected void restart(Set<URL> urls, ClassLoaderFiles files) {
176                Restarter restarter = Restarter.getInstance();
177                restarter.addUrls(urls);
178                restarter.addClassLoaderFiles(files);
179                restarter.restart();
180        }
181
182}