001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.util.watch;
018
019import java.time.Duration;
020import java.time.Instant;
021import java.util.Set;
022import java.util.concurrent.ConcurrentHashMap;
023
024import org.apache.wicket.util.lang.Generics;
025import org.apache.wicket.util.listener.ChangeListenerSet;
026import org.apache.wicket.util.listener.IChangeListener;
027import org.apache.wicket.util.thread.ICode;
028import org.apache.wicket.util.thread.Task;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032
033/**
034 * Monitors one or more <code>IModifiable</code> objects, calling a {@link IChangeListener
035 * IChangeListener} when a given object's modification time changes.
036 * 
037 * @author Jonathan Locke
038 * @since 1.2.6
039 */
040public class ModificationWatcher implements IModificationWatcher
041{
042        /** logger */
043        private static final Logger log = LoggerFactory.getLogger(ModificationWatcher.class);
044
045        /** maps <code>IModifiable</code> objects to <code>Entry</code> objects */
046        private final ConcurrentHashMap<IModifiable, Entry> modifiableToEntry = Generics.newConcurrentHashMap();
047
048        /** the <code>Task</code> to run */
049        private Task task;
050
051        /**
052         * Container class for holding modifiable entries to watch.
053         */
054        protected static final class Entry
055        {
056                // The most recent lastModificationTime polled on the object
057                public Instant lastModifiedTime;
058
059                // The set of listeners to call when the modifiable changes
060                public final ChangeListenerSet<IModifiable> listeners = new ChangeListenerSet<>();
061
062                // The modifiable thing
063                public IModifiable modifiable;
064        }
065
066        /**
067         * Default constructor for two-phase construction.
068         */
069        public ModificationWatcher()
070        {
071        }
072
073        /**
074         * Constructor that accepts a <code>Duration</code> argument representing the poll frequency.
075         * 
076         * @param pollFrequency
077         *            how often to check on <code>IModifiable</code>s
078         */
079        public ModificationWatcher(final Duration pollFrequency)
080        {
081                start(pollFrequency);
082        }
083
084        @Override
085        public final boolean add(final IModifiable modifiable, final IChangeListener<IModifiable> listener)
086        {
087                // Look up entry for modifiable
088                final Entry entry = modifiableToEntry.get(modifiable);
089
090                // Found it?
091                if (entry == null)
092                {
093                        Instant lastModifiedTime = modifiable.lastModifiedTime();
094                        if (lastModifiedTime != null)
095                        {
096                                // Construct new entry
097                                final Entry newEntry = new Entry();
098
099                                newEntry.modifiable = modifiable;
100                                newEntry.lastModifiedTime = lastModifiedTime;
101                                newEntry.listeners.add(listener);
102
103                                // Put in map
104                                modifiableToEntry.putIfAbsent(modifiable, newEntry);
105                        }
106                        else
107                        {
108                                // The IModifiable is not returning a valid lastModifiedTime
109                                log.info("Cannot track modifications to resource '{}'", modifiable);
110                        }
111
112                        return true;
113                }
114                else
115                {
116                        // Add listener to existing entry
117                        return !entry.listeners.add(listener);
118                }
119        }
120
121        @Override
122        public IModifiable remove(final IModifiable modifiable)
123        {
124                final Entry entry = modifiableToEntry.remove(modifiable);
125                if (entry != null)
126                {
127                        return entry.modifiable;
128                }
129                return null;
130        }
131
132        @Override
133        public void start(final Duration pollFrequency)
134        {
135                // Construct task with the given polling frequency
136                task = new Task("ModificationWatcher");
137
138                task.run(pollFrequency, new ICode()
139                {
140                        @Override
141                        public void run(final Logger log)
142                        {
143                                checkModified();
144                        }
145                });
146        }
147
148        /**
149         * Checks which IModifiables were modified and notifies their listeners
150         */
151        protected void checkModified()
152        {
153                for (Entry entry : modifiableToEntry.values())
154                {
155                        // If the modifiable has been modified after the last known
156                        // modification time
157                        final Instant modifiableLastModified = entry.modifiable.lastModifiedTime();
158                        if ((modifiableLastModified != null) &&
159                                        modifiableLastModified.isAfter(entry.lastModifiedTime))
160                        {
161                                // Notify all listeners that the modifiable was modified
162                                entry.listeners.notifyListeners(entry.modifiable);
163
164                                // Update timestamp
165                                entry.lastModifiedTime = modifiableLastModified;
166                        }
167                }
168        }
169
170        @Override
171        public void destroy()
172        {
173                if (task != null)
174                {
175                        // task.stop();
176                        task.interrupt();
177                }
178        }
179
180        @Override
181        public final Set<IModifiable> getEntries()
182        {
183                return modifiableToEntry.keySet();
184        }
185}