    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}