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}