001// Copyright 2011, 2012, 2022 The Apache Software Foundation 002// 003// Licensed under the Apache License, Version 2.0 (the "License"); 004// you may not use this file except in compliance with the License. 005// You may obtain a copy of the License at 006// 007// http://www.apache.org/licenses/LICENSE-2.0 008// 009// Unless required by applicable law or agreed to in writing, software 010// distributed under the License is distributed on an "AS IS" BASIS, 011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012// See the License for the specific language governing permissions and 013// limitations under the License. 014 015package org.apache.tapestry5.internal.services.assets; 016 017import java.util.ArrayList; 018import java.util.List; 019import java.util.Objects; 020import java.util.Set; 021import java.util.stream.Collectors; 022 023import org.apache.tapestry5.SymbolConstants; 024import org.apache.tapestry5.commons.Resource; 025import org.apache.tapestry5.http.TapestryHttpSymbolConstants; 026import org.apache.tapestry5.internal.event.InvalidationEventHubImpl; 027import org.apache.tapestry5.internal.services.ClassNameHolder; 028import org.apache.tapestry5.ioc.annotations.PostInjection; 029import org.apache.tapestry5.ioc.annotations.Symbol; 030import org.apache.tapestry5.ioc.internal.util.URLChangeTracker; 031import org.apache.tapestry5.ioc.services.ClasspathURLConverter; 032import org.apache.tapestry5.ioc.services.UpdateListener; 033import org.apache.tapestry5.ioc.services.UpdateListenerHub; 034import org.slf4j.Logger; 035 036public class ResourceChangeTrackerImpl extends InvalidationEventHubImpl implements ResourceChangeTracker, 037 UpdateListener 038{ 039 private final URLChangeTracker<ResourceInfo> tracker; 040 041 private final ThreadLocal<String> currentClassName; 042 043 private final Logger logger; 044 045 private final boolean multipleClassLoaders; 046 047 /** 048 * Used in production mode as the last modified time of any resource exposed to the client. Remember that 049 * all exposed assets include a URL with a version number, and each new deployment of the application should change 050 * that version number. 051 */ 052 private final long fixedLastModifiedTime = Math.round(System.currentTimeMillis() / 1000d) * 1000L; 053 054 public ResourceChangeTrackerImpl(ClasspathURLConverter classpathURLConverter, 055 @Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE) 056 boolean productionMode, 057 @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) 058 boolean multipleClassLoaders, Logger logger) 059 { 060 super(productionMode, logger); 061 this.logger = logger; 062 this.multipleClassLoaders = multipleClassLoaders; 063 064 // Use granularity of seconds (not milliseconds) since that works properly 065 // with response headers for identifying last modified. Don't track 066 // folder changes, just changes to actual files. 067 tracker = productionMode ? null : new URLChangeTracker<ResourceInfo>(classpathURLConverter, true, false); 068 currentClassName = productionMode ? null : new ThreadLocal<>(); 069 } 070 071 @PostInjection 072 public void registerWithUpdateListenerHub(UpdateListenerHub hub) 073 { 074 hub.addUpdateListener(this); 075 } 076 077 078 public long trackResource(Resource resource) 079 { 080 if (tracker == null) 081 { 082 return fixedLastModifiedTime; 083 } 084 085 return tracker.add(resource.toURL(), new ResourceInfo(resource.toString(), currentClassName.get())); 086 } 087 088 public void addDependency(Resource dependency) 089 { 090 trackResource(dependency); 091 } 092 093 public void forceInvalidationEvent() 094 { 095 fireInvalidationEvent(); 096 097 if (tracker != null) 098 { 099 tracker.clear(); 100 } 101 } 102 103 public void checkForUpdates() 104 { 105 if (tracker != null) 106 { 107 final Set<ResourceInfo> changedResources = tracker.getChangedResourcesInfo(); 108 if (!changedResources.isEmpty()) 109 { 110 logger.info("Changed resources: {}", changedResources.stream() 111 .map(ResourceInfo::getResource) 112 .collect(Collectors.joining(", "))); 113 } 114 115 boolean applicationLevelChange = false; 116 117 for (ResourceInfo info : changedResources) 118 { 119 120 // An application-level file was changed, so we need to invalidate everything. 121 if (info.getClassName() == null || !multipleClassLoaders) 122 { 123 forceInvalidationEvent(); 124 applicationLevelChange = true; 125 break; 126 } 127 128 } 129 130 if (!changedResources.isEmpty() && !applicationLevelChange) 131 { 132 List<String> resources = new ArrayList<>(4); 133 resources.addAll(changedResources.stream() 134 .filter(Objects::nonNull) 135 .map(ResourceInfo::getResource) 136 .filter(Objects::nonNull) 137 .collect(Collectors.toList())); 138 resources.addAll(changedResources.stream() 139 .filter(Objects::nonNull) 140 .map(ClassNameHolder::getClassName) 141 .filter(Objects::nonNull) 142 .collect(Collectors.toList())); 143 fireInvalidationEvent(resources); 144 } 145 } 146 } 147 148 @Override 149 public void setCurrentClassName(String className) 150 { 151 if (currentClassName != null) 152 { 153 currentClassName.set(className); 154 } 155 } 156 157 @Override 158 public void clearCurrentClassName() 159 { 160 if (currentClassName != null) 161 { 162 currentClassName.set(null); 163 } 164 } 165 166 private static class ResourceInfo implements ClassNameHolder 167 { 168 private String resource; 169 private String className; 170 171 public ResourceInfo(String resource, String className) 172 { 173 super(); 174 this.className = className; 175 this.resource = resource; 176 } 177 178 @Override 179 public int hashCode() 180 { 181 return Objects.hash(className, resource); 182 } 183 184 @Override 185 public boolean equals(Object obj) 186 { 187 if (this == obj) 188 return true; 189 if (obj == null) 190 return false; 191 if (getClass() != obj.getClass()) 192 return false; 193 ResourceInfo other = (ResourceInfo) obj; 194 return Objects.equals(className, other.className) && Objects.equals(resource, other.resource); 195 } 196 197 @Override 198 public String toString() { 199 return "ResourceInfo [path=" + resource + ", className=" + className + "]"; 200 } 201 202 public String getResource() 203 { 204 return resource; 205 } 206 207 public String getClassName() 208 { 209 return className; 210 } 211 212 } 213 214}