f10bdf0d754a9ae2a1e597de152ee65ba1bb8c38
[phpeclipse.git] / net.sourceforge.phpeclipse / src / net / sourceforge / phpdt / internal / core / Buffer.java
1 /*******************************************************************************
2  * Copyright (c) 2000, 2003 IBM Corporation and others.
3  * All rights reserved. This program and the accompanying materials 
4  * are made available under the terms of the Common Public License v1.0
5  * which accompanies this distribution, and is available at
6  * http://www.eclipse.org/legal/cpl-v10.html
7  * 
8  * Contributors:
9  *     IBM Corporation - initial API and implementation
10  *******************************************************************************/
11 package net.sourceforge.phpdt.internal.core;
12
13 import java.io.ByteArrayInputStream;
14 import java.io.IOException;
15 import java.util.ArrayList;
16
17 import net.sourceforge.phpdt.core.BufferChangedEvent;
18 import net.sourceforge.phpdt.core.IBuffer;
19 import net.sourceforge.phpdt.core.IBufferChangedListener;
20 import net.sourceforge.phpdt.core.IJavaModelStatusConstants;
21 import net.sourceforge.phpdt.core.IOpenable;
22 import net.sourceforge.phpdt.core.JavaModelException;
23 import net.sourceforge.phpdt.internal.core.util.Util;
24
25 import org.eclipse.core.resources.IFile;
26 import org.eclipse.core.resources.IResource;
27 import org.eclipse.core.runtime.CoreException;
28 import org.eclipse.core.runtime.IProgressMonitor;
29 import org.eclipse.core.runtime.ISafeRunnable;
30 import org.eclipse.core.runtime.Platform;
31
32 /**
33  * @see IBuffer
34  */
35 public class Buffer implements IBuffer {
36         protected IFile file;
37
38         protected int flags;
39
40         protected char[] contents;
41
42         protected ArrayList changeListeners;
43
44         protected IOpenable owner;
45
46         protected int gapStart = -1;
47
48         protected int gapEnd = -1;
49
50         protected Object lock = new Object();
51
52         protected static final int F_HAS_UNSAVED_CHANGES = 1;
53
54         protected static final int F_IS_READ_ONLY = 2;
55
56         protected static final int F_IS_CLOSED = 4;
57
58         /**
59          * Creates a new buffer on an underlying resource.
60          */
61         protected Buffer(IFile file, IOpenable owner, boolean readOnly) {
62                 this.file = file;
63                 this.owner = owner;
64                 if (file == null) {
65                         setReadOnly(readOnly);
66                 }
67         }
68
69         /**
70          * @see IBuffer
71          */
72         public void addBufferChangedListener(IBufferChangedListener listener) {
73                 if (this.changeListeners == null) {
74                         this.changeListeners = new ArrayList(5);
75                 }
76                 if (!this.changeListeners.contains(listener)) {
77                         this.changeListeners.add(listener);
78                 }
79         }
80
81         /**
82          * Append the <code>text</code> to the actual content, the gap is moved to
83          * the end of the <code>text</code>.
84          */
85         public void append(char[] text) {
86                 if (!isReadOnly()) {
87                         if (text == null || text.length == 0) {
88                                 return;
89                         }
90                         int length = getLength();
91                         moveAndResizeGap(length, text.length);
92                         System.arraycopy(text, 0, this.contents, length, text.length);
93                         this.gapStart += text.length;
94                         this.flags |= F_HAS_UNSAVED_CHANGES;
95                         notifyChanged(new BufferChangedEvent(this, length, 0, new String(
96                                         text)));
97                 }
98         }
99
100         /**
101          * Append the <code>text</code> to the actual content, the gap is moved to
102          * the end of the <code>text</code>.
103          */
104         public void append(String text) {
105                 if (text == null) {
106                         return;
107                 }
108                 this.append(text.toCharArray());
109         }
110
111         /**
112          * @see IBuffer
113          */
114         public void close() throws IllegalArgumentException {
115                 BufferChangedEvent event = null;
116                 synchronized (this.lock) {
117                         if (isClosed())
118                                 return;
119                         event = new BufferChangedEvent(this, 0, 0, null);
120                         this.contents = null;
121                         this.flags |= F_IS_CLOSED;
122                 }
123                 notifyChanged(event); // notify outside of synchronized block
124                 this.changeListeners = null;
125         }
126
127         /**
128          * @see IBuffer
129          */
130         public char getChar(int position) {
131                 synchronized (this.lock) {
132                         if (position < this.gapStart) {
133                                 return this.contents[position];
134                         }
135                         int gapLength = this.gapEnd - this.gapStart;
136                         return this.contents[position + gapLength];
137                 }
138         }
139
140         /**
141          * @see IBuffer
142          */
143         public char[] getCharacters() {
144                 if (this.contents == null)
145                         return null;
146                 synchronized (this.lock) {
147                         if (this.gapStart < 0) {
148                                 return this.contents;
149                         }
150                         int length = this.contents.length;
151                         char[] newContents = new char[length - this.gapEnd + this.gapStart];
152                         System.arraycopy(this.contents, 0, newContents, 0, this.gapStart);
153                         System.arraycopy(this.contents, this.gapEnd, newContents,
154                                         this.gapStart, length - this.gapEnd);
155                         return newContents;
156                 }
157         }
158
159         /**
160          * @see IBuffer
161          */
162         public String getContents() {
163                 char[] chars = this.getCharacters();
164                 if (chars == null)
165                         return null;
166                 return new String(chars);
167         }
168
169         /**
170          * @see IBuffer
171          */
172         public int getLength() {
173                 synchronized (this.lock) {
174                         int length = this.gapEnd - this.gapStart;
175                         return (this.contents.length - length);
176                 }
177         }
178
179         /**
180          * @see IBuffer
181          */
182         public IOpenable getOwner() {
183                 return this.owner;
184         }
185
186         /**
187          * @see IBuffer
188          */
189         public String getText(int offset, int length) {
190                 if (this.contents == null)
191                         return ""; //$NON-NLS-1$
192                 synchronized (this.lock) {
193                         if (offset + length < this.gapStart)
194                                 return new String(this.contents, offset, length);
195                         if (this.gapStart < offset) {
196                                 int gapLength = this.gapEnd - this.gapStart;
197                                 return new String(this.contents, offset + gapLength, length);
198                         }
199                         StringBuffer buf = new StringBuffer();
200                         buf.append(this.contents, offset, this.gapStart - offset);
201                         buf.append(this.contents, this.gapEnd, offset + length
202                                         - this.gapStart);
203                         return buf.toString();
204                 }
205         }
206
207         /**
208          * @see IBuffer
209          */
210         public IResource getUnderlyingResource() {
211                 return this.file;
212         }
213
214         /**
215          * @see IBuffer
216          */
217         public boolean hasUnsavedChanges() {
218                 return (this.flags & F_HAS_UNSAVED_CHANGES) != 0;
219         }
220
221         /**
222          * @see IBuffer
223          */
224         public boolean isClosed() {
225                 return (this.flags & F_IS_CLOSED) != 0;
226         }
227
228         /**
229          * @see IBuffer
230          */
231         public boolean isReadOnly() {
232                 if (this.file == null) {
233                         return (this.flags & F_IS_READ_ONLY) != 0;
234                 } else {
235                         return this.file.isReadOnly();
236                 }
237         }
238
239         /**
240          * Moves the gap to location and adjust its size to the anticipated change
241          * size. The size represents the expected range of the gap that will be
242          * filled after the gap has been moved. Thus the gap is resized to actual
243          * size + the specified size and moved to the given position.
244          */
245         protected void moveAndResizeGap(int position, int size) {
246                 char[] content = null;
247                 int oldSize = this.gapEnd - this.gapStart;
248                 if (size < 0) {
249                         if (oldSize > 0) {
250                                 content = new char[this.contents.length - oldSize];
251                                 System.arraycopy(this.contents, 0, content, 0, this.gapStart);
252                                 System.arraycopy(this.contents, this.gapEnd, content,
253                                                 this.gapStart, content.length - this.gapStart);
254                                 this.contents = content;
255                         }
256                         this.gapStart = this.gapEnd = position;
257                         return;
258                 }
259                 content = new char[this.contents.length + (size - oldSize)];
260                 int newGapStart = position;
261                 int newGapEnd = newGapStart + size;
262                 if (oldSize == 0) {
263                         System.arraycopy(this.contents, 0, content, 0, newGapStart);
264                         System.arraycopy(this.contents, newGapStart, content, newGapEnd,
265                                         content.length - newGapEnd);
266                 } else if (newGapStart < this.gapStart) {
267                         int delta = this.gapStart - newGapStart;
268                         System.arraycopy(this.contents, 0, content, 0, newGapStart);
269                         System.arraycopy(this.contents, newGapStart, content, newGapEnd,
270                                         delta);
271                         System.arraycopy(this.contents, this.gapEnd, content, newGapEnd
272                                         + delta, this.contents.length - this.gapEnd);
273                 } else {
274                         int delta = newGapStart - this.gapStart;
275                         System.arraycopy(this.contents, 0, content, 0, this.gapStart);
276                         System.arraycopy(this.contents, this.gapEnd, content,
277                                         this.gapStart, delta);
278                         System.arraycopy(this.contents, this.gapEnd + delta, content,
279                                         newGapEnd, content.length - newGapEnd);
280                 }
281                 this.contents = content;
282                 this.gapStart = newGapStart;
283                 this.gapEnd = newGapEnd;
284         }
285
286         /**
287          * Notify the listeners that this buffer has changed. To avoid deadlock,
288          * this should not be called in a synchronized block.
289          */
290         protected void notifyChanged(final BufferChangedEvent event) {
291                 if (this.changeListeners != null) {
292                         for (int i = 0, size = this.changeListeners.size(); i < size; ++i) {
293                                 final IBufferChangedListener listener = (IBufferChangedListener) this.changeListeners
294                                                 .get(i);
295                                 Platform.run(new ISafeRunnable() {
296                                         public void handleException(Throwable exception) {
297                                                 Util
298                                                                 .log(exception,
299                                                                                 "Exception occurred in listener of buffer change notification"); //$NON-NLS-1$
300                                         }
301
302                                         public void run() throws Exception {
303                                                 listener.bufferChanged(event);
304                                         }
305                                 });
306
307                         }
308                 }
309         }
310
311         /**
312          * @see IBuffer
313          */
314         public void removeBufferChangedListener(IBufferChangedListener listener) {
315                 if (this.changeListeners != null) {
316                         this.changeListeners.remove(listener);
317                         if (this.changeListeners.size() == 0) {
318                                 this.changeListeners = null;
319                         }
320                 }
321         }
322
323         /**
324          * Replaces <code>length</code> characters starting from
325          * <code>position</code> with <code>text<code>.
326          * After that operation, the gap is placed at the end of the 
327          * inserted <code>text</code>.
328          */
329         public void replace(int position, int length, char[] text) {
330                 if (!isReadOnly()) {
331                         int textLength = text == null ? 0 : text.length;
332                         synchronized (this.lock) {
333                                 // move gap
334                                 moveAndResizeGap(position + length, textLength - length);
335
336                                 // overwrite
337                                 int min = Math.min(textLength, length);
338                                 if (min > 0) {
339                                         System.arraycopy(text, 0, this.contents, position, min);
340                                 }
341                                 if (length > textLength) {
342                                         // enlarge the gap
343                                         this.gapStart -= length - textLength;
344                                 } else if (textLength > length) {
345                                         // shrink gap
346                                         this.gapStart += textLength - length;
347                                         System.arraycopy(text, 0, this.contents, position,
348                                                         textLength);
349                                 }
350                         }
351                         this.flags |= F_HAS_UNSAVED_CHANGES;
352                         String string = null;
353                         if (textLength > 0) {
354                                 string = new String(text);
355                         }
356                         notifyChanged(new BufferChangedEvent(this, position, length, string));
357                 }
358         }
359
360         /**
361          * Replaces <code>length</code> characters starting from
362          * <code>position</code> with <code>text<code>.
363          * After that operation, the gap is placed at the end of the 
364          * inserted <code>text</code>.
365          */
366         public void replace(int position, int length, String text) {
367                 this
368                                 .replace(position, length, text == null ? null : text
369                                                 .toCharArray());
370         }
371
372         /**
373          * @see IBuffer
374          */
375         public void save(IProgressMonitor progress, boolean force)
376                         throws JavaModelException {
377
378                 // determine if saving is required
379                 if (isReadOnly() || this.file == null) {
380                         return;
381                 }
382                 synchronized (this.lock) {
383                         if (!hasUnsavedChanges())
384                                 return;
385
386                         // use a platform operation to update the resource contents
387                         try {
388                                 // String encoding =
389                                 // ((IJavaElement)this.owner).getJavaProject().getOption(PHPCore.CORE_ENCODING,
390                                 // true);
391                                 String encoding = null;
392                                 String contents = this.getContents();
393                                 if (contents == null)
394                                         return;
395                                 byte[] bytes = encoding == null ? contents.getBytes()
396                                                 : contents.getBytes(encoding);
397                                 ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
398
399                                 this.file
400                                                 .setContents(stream, force ? IResource.FORCE
401                                                                 | IResource.KEEP_HISTORY
402                                                                 : IResource.KEEP_HISTORY, null);
403                         } catch (IOException e) {
404                                 throw new JavaModelException(e,
405                                                 IJavaModelStatusConstants.IO_EXCEPTION);
406                         } catch (CoreException e) {
407                                 throw new JavaModelException(e);
408                         }
409
410                         // the resource no longer has unsaved changes
411                         this.flags &= ~(F_HAS_UNSAVED_CHANGES);
412                 }
413         }
414
415         /**
416          * @see IBuffer
417          */
418         public void setContents(char[] newContents) {
419                 // allow special case for first initialization
420                 // after creation by buffer factory
421                 if (this.contents == null) {
422                         this.contents = newContents;
423                         this.flags &= ~(F_HAS_UNSAVED_CHANGES);
424                         return;
425                 }
426
427                 if (!isReadOnly()) {
428                         String string = null;
429                         if (newContents != null) {
430                                 string = new String(newContents);
431                         }
432                         BufferChangedEvent event = new BufferChangedEvent(this, 0, this
433                                         .getLength(), string);
434                         synchronized (this.lock) {
435                                 this.contents = newContents;
436                                 this.flags |= F_HAS_UNSAVED_CHANGES;
437                                 this.gapStart = -1;
438                                 this.gapEnd = -1;
439                         }
440                         notifyChanged(event);
441                 }
442         }
443
444         /**
445          * @see IBuffer
446          */
447         public void setContents(String newContents) {
448                 this.setContents(newContents.toCharArray());
449         }
450
451         /**
452          * Sets this <code>Buffer</code> to be read only.
453          */
454         protected void setReadOnly(boolean readOnly) {
455                 if (readOnly) {
456                         this.flags |= F_IS_READ_ONLY;
457                 } else {
458                         this.flags &= ~(F_IS_READ_ONLY);
459                 }
460         }
461
462         public String toString() {
463                 StringBuffer buffer = new StringBuffer();
464                 // buffer.append("Owner: " +
465                 // ((JavaElement)this.owner).toStringWithAncestors()); //$NON-NLS-1$
466                 buffer.append("Owner: " + (this.owner).toString()); //$NON-NLS-1$
467                 buffer.append("\nHas unsaved changes: " + this.hasUnsavedChanges()); //$NON-NLS-1$
468                 buffer.append("\nIs readonly: " + this.isReadOnly()); //$NON-NLS-1$
469                 buffer.append("\nIs closed: " + this.isClosed()); //$NON-NLS-1$
470                 buffer.append("\nContents:\n"); //$NON-NLS-1$
471                 char[] contents = this.getCharacters();
472                 if (contents == null) {
473                         buffer.append("<null>"); //$NON-NLS-1$
474                 } else {
475                         int length = contents.length;
476                         for (int i = 0; i < length; i++) {
477                                 char car = contents[i];
478                                 switch (car) {
479                                 case '\n':
480                                         buffer.append("\\n\n"); //$NON-NLS-1$
481                                         break;
482                                 case '\r':
483                                         if (i < length - 1 && this.contents[i + 1] == '\n') {
484                                                 buffer.append("\\r\\n\n"); //$NON-NLS-1$
485                                                 i++;
486                                         } else {
487                                                 buffer.append("\\r\n"); //$NON-NLS-1$
488                                         }
489                                         break;
490                                 default:
491                                         buffer.append(car);
492                                         break;
493                                 }
494                         }
495                 }
496                 return buffer.toString();
497         }
498 }