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}