| History.java |
1 /**
2 * Distribution License:
3 * JSword is free software; you can redistribute it and/or modify it under
4 * the terms of the GNU Lesser General Public License, version 2.1 as published by
5 * the Free Software Foundation. This program is distributed in the hope
6 * that it will be useful, but WITHOUT ANY WARRANTY; without even the
7 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8 * See the GNU Lesser General Public License for more details.
9 *
10 * The License is available on the internet at:
11 * http://www.gnu.org/copyleft/lgpl.html
12 * or by writing to:
13 * Free Software Foundation, Inc.
14 * 59 Temple Place - Suite 330
15 * Boston, MA 02111-1307, USA
16 *
17 * Copyright: 2005
18 * The copyright to this program is held by it's authors.
19 *
20 * ID: $Id: History.java 2090 2011-03-07 04:13:05Z dmsmith $
21 */
22 package org.crosswire.common.history;
23
24 import java.util.ArrayList;
25 import java.util.Collections;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Map;
29
30 import org.crosswire.common.util.EventListenerList;
31
32 /**
33 * Maintains a navigable history of objects. This maintains a dated list of
34 * objects and a current navigation list.
35 *
36 * @see gnu.lgpl.License for license details.<br>
37 * The copyright to this program is held by it's authors.
38 * @author DM Smith [dmsmith555 at yahoo dot com]
39 */
40
41 public class History {
42 /**
43 * Create an empty navigation and history list.
44 *
45 */
46 public History() {
47 nav = new ArrayList<Object>();
48 history = new HashMap<Object, Long>();
49 listeners = new EventListenerList();
50 }
51
52 /**
53 * Make a particular element in the navigation list the current item in
54 * history.
55 *
56 * @param index
57 * the index of item to make the last one in the back list, -1
58 * (or lower) will put everything in the forward list. Indexes
59 * beyond the end of the list will put everything in the back
60 * list.
61 */
62 public Object select(int index) {
63 int i = index;
64
65 // Adjust to be 1 based
66 int size = nav.size();
67
68 if (i > size) {
69 i = size;
70 } else if (i < 1) {
71 i = 1;
72 }
73
74 // Only fire history changes when there is a change.
75 if (i != backCount) {
76 backCount = i;
77 fireHistoryChanged();
78 }
79
80 return getCurrent();
81 }
82
83 /**
84 * Add an element to history. If the element is in the forward list, then it
85 * replaces everything in the forward list upto it. Otherwise, it replaces
86 * the forward list.
87 *
88 * @param obj
89 * the object to add
90 */
91 public void add(Object obj) {
92 Object current = getCurrent();
93
94 // Don't add null objects or the same object.
95 if (obj == null || obj.equals(current)) {
96 return;
97 }
98
99 // If we are adding the next element, then just advance
100 // otherwise ...
101 // Object next = peek(1);
102 // if (!obj.equals(next))
103 // {
104 int size = nav.size();
105 if (size > backCount) {
106 int pos = backCount;
107 while (pos < size && !obj.equals(nav.get(pos))) {
108 pos++;
109 }
110 // At this point pos either == size or the element at pos matches
111 // what we are navigating to.
112 nav.subList(backCount, Math.min(pos, size)).clear();
113 }
114
115 // If it matches, then we don't have to do anything more
116 if (!obj.equals(peek(1))) {
117 // then we add it
118 nav.add(backCount, obj);
119 }
120 // }
121
122 backCount++;
123
124 // and remember when we saw it
125 visit(obj);
126
127 fireHistoryChanged();
128 }
129
130 /**
131 * Get all the elements in "back" list.
132 *
133 * @return the elements in the back list.
134 */
135 public List<Object> getPreviousList() {
136 if (backCount > 0) {
137 return Collections.unmodifiableList(nav.subList(0, backCount));
138 }
139 return Collections.emptyList();
140 }
141
142 /**
143 * Get all the elements in the "forward" list.
144 *
145 * @return the elements in the forward list.
146 */
147 public List<Object> getNextList() {
148 if (backCount < nav.size()) {
149 return Collections.unmodifiableList(nav.subList(backCount, nav.size()));
150 }
151 return Collections.emptyList();
152 }
153
154 /**
155 * Increments the current history item by the given amount. Positive numbers
156 * are forward. Negative numbers are back.
157 *
158 * @param i
159 * the distance to travel
160 * @return the item at the requested location, or at the end of the list if
161 * i is too big, or at the beginning of the list if i is too small,
162 * otherwise null.
163 */
164 public Object go(int i) {
165 return select(backCount + i);
166 }
167
168 /**
169 * Get the current item in the "back" list
170 *
171 * @return the current item in the back list.
172 */
173 public Object getCurrent() {
174 if (!nav.isEmpty() && backCount > 0) {
175 return nav.get(backCount - 1);
176 }
177 return null;
178 }
179
180 /**
181 * Get the current item in the "back" list
182 *
183 * @param i
184 * the distance to travel
185 * @return the requested item in the navigation list.
186 */
187 private Object peek(int i) {
188 int size = nav.size();
189 if (size > 0 && backCount > 0 && backCount + i <= size) {
190 return nav.get(backCount + i - 1);
191 }
192 return null;
193 }
194
195 /**
196 * Add a listener for history events.
197 *
198 * @param li
199 * the interested listener
200 */
201 public synchronized void addHistoryListener(HistoryListener li) {
202 listeners.add(HistoryListener.class, li);
203 }
204
205 /**
206 * Remove a listener of history events.
207 *
208 * @param li
209 * the disinterested listener
210 */
211 public synchronized void removeHistoryListener(HistoryListener li) {
212 listeners.remove(HistoryListener.class, li);
213 }
214
215 /**
216 * Note that this object has been seen at this time.
217 *
218 * @param obj
219 */
220 private void visit(Object obj) {
221 history.put(obj, Long.valueOf(System.currentTimeMillis()));
222 }
223
224 /**
225 * Kick of an event sequence
226 */
227 private synchronized void fireHistoryChanged() {
228 // Guaranteed to return a non-null array
229 Object[] contents = listeners.getListenerList();
230
231 // Process the listeners last to first, notifying
232 // those that are interested in this event
233 HistoryEvent ev = null;
234 for (int i = contents.length - 2; i >= 0; i -= 2) {
235 if (contents[i] == HistoryListener.class) {
236 if (ev == null) {
237 ev = new HistoryEvent(this);
238 }
239
240 ((HistoryListener) contents[i + 1]).historyChanged(ev);
241 }
242 }
243 }
244
245 /**
246 * The elements that can be navigated.
247 */
248 private List<Object> nav;
249
250 /**
251 * A map of elements that have been seen so far to when they have been seen.
252 */
253 private Map<Object, Long> history;
254
255 /**
256 * The number of elements in the "back" list.
257 */
258 private int backCount;
259
260 /**
261 * Listeners that are interested when history has changed.
262 */
263 private EventListenerList listeners;
264 }
265