001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.modules.editor.lib2.highlighting;
043:
044: import java.beans.PropertyChangeEvent;
045: import java.beans.PropertyChangeListener;
046: import java.lang.ref.WeakReference;
047: import java.util.ArrayList;
048: import java.util.Arrays;
049: import java.util.Collection;
050: import java.util.Collections;
051: import java.util.HashMap;
052: import java.util.List;
053: import java.util.WeakHashMap;
054: import java.util.logging.Level;
055: import java.util.logging.Logger;
056: import java.util.regex.Pattern;
057: import java.util.regex.PatternSyntaxException;
058: import javax.swing.text.Document;
059: import javax.swing.text.JTextComponent;
060: import org.netbeans.api.editor.mimelookup.MimeLookup;
061: import org.netbeans.api.editor.mimelookup.MimePath;
062: import org.netbeans.api.editor.settings.FontColorSettings;
063: import org.netbeans.spi.editor.highlighting.HighlightsContainer;
064: import org.netbeans.spi.editor.highlighting.HighlightsLayer;
065: import org.netbeans.spi.editor.highlighting.HighlightsLayerFactory;
066: import org.openide.ErrorManager;
067: import org.openide.util.Lookup;
068: import org.openide.util.LookupEvent;
069: import org.openide.util.LookupListener;
070: import org.openide.util.TopologicalSortException;
071: import org.openide.util.Utilities;
072: import org.openide.util.WeakListeners;
073: import org.openide.util.lookup.ProxyLookup;
074:
075: /**
076: *
077: * @author Vita Stejskal
078: */
079: public final class HighlightingManager {
080:
081: // -J-Dorg.netbeans.modules.editor.lib2.highlighting.HighlightingManager.level=300
082: private static final Logger LOG = Logger
083: .getLogger(HighlightingManager.class.getName());
084:
085: public static synchronized HighlightingManager getInstance() {
086: if (instance == null) {
087: instance = new HighlightingManager();
088: }
089: return instance;
090: }
091:
092: public synchronized HighlightsContainer getHighlights(
093: JTextComponent pane, HighlightsLayerFilter filter) {
094: Highlighting h = (Highlighting) pane
095: .getClientProperty(Highlighting.class);
096: if (h == null) {
097: h = new Highlighting(pane);
098: pane.putClientProperty(Highlighting.class, h);
099: }
100: return h
101: .getContainer(filter == null ? HighlightsLayerFilter.IDENTITY
102: : filter);
103: }
104:
105: // ----------------------------------------------------------------------
106: // Private implementation
107: // ----------------------------------------------------------------------
108:
109: private static HighlightingManager instance;
110:
111: /** Creates a new instance of HighlightingManager */
112: private HighlightingManager() {
113: }
114:
115: private static final class Highlighting implements
116: PropertyChangeListener {
117:
118: private static final String PROP_MIME_TYPE = "mimeType"; //NOI18N
119: private static final String PROP_DOCUMENT = "document"; //NOI18N
120: private static final String PROP_HL_INCLUDES = "HighlightsLayerIncludes"; //NOI18N
121: private static final String PROP_HL_EXCLUDES = "HighlightsLayerExcludes"; //NOI18N
122:
123: // The factories changes tracking
124: private Lookup.Result<HighlightsLayerFactory> factories = null;
125: private LookupListener factoriesTracker = new LookupListener() {
126: public void resultChanged(LookupEvent ev) {
127: rebuildAllLayers();
128: }
129: };
130:
131: // The FontColorSettings changes tracking
132: private Lookup.Result<FontColorSettings> settings = null;
133: private LookupListener settingsTracker = new LookupListener() {
134: public void resultChanged(LookupEvent ev) {
135: // System.out.println("Settings tracker for '" + (lastKnownMimePaths == null ? "null" : lastKnownMimePaths[0].getPath()) + "'");
136: rebuildAllLayers();
137: }
138: };
139:
140: private final JTextComponent pane;
141: private HighlightsLayerFilter paneFilter;
142: private Document lastKnownDocument = null;
143: private MimePath[] lastKnownMimePaths = null;
144: private boolean inRebuildAllLayers = false;
145:
146: // all layers (sorted, but without filtering) and their HighlightsContainers
147: private List<? extends HighlightsLayer> allLayers = null;
148: private List<HighlightsContainer> allLayerContainers = null;
149:
150: // CompoundHighlightsContainers with containers from filtered layers
151: private final WeakHashMap<HighlightsLayerFilter, WeakReference<CompoundHighlightsContainer>> containers = new WeakHashMap<HighlightsLayerFilter, WeakReference<CompoundHighlightsContainer>>();
152:
153: public Highlighting(JTextComponent pane) {
154: this .pane = pane;
155: this .paneFilter = new RegExpFilter(pane
156: .getClientProperty(PROP_HL_INCLUDES), pane
157: .getClientProperty(PROP_HL_EXCLUDES));
158: this .pane.addPropertyChangeListener(WeakListeners
159: .propertyChange(this , pane));
160:
161: rebuildAll();
162: }
163:
164: public synchronized HighlightsContainer getContainer(
165: HighlightsLayerFilter filter) {
166: WeakReference<CompoundHighlightsContainer> ref = containers
167: .get(filter);
168: CompoundHighlightsContainer container = ref == null ? null
169: : ref.get();
170:
171: if (container == null) {
172: container = new CompoundHighlightsContainer();
173: rebuildContainer(pane.getDocument(), filter, container);
174:
175: containers.put(filter,
176: new WeakReference<CompoundHighlightsContainer>(
177: container));
178: }
179:
180: return container;
181: }
182:
183: // ----------------------------------------------------------------------
184: // PropertyChangeListener implementation
185: // ----------------------------------------------------------------------
186:
187: public void propertyChange(PropertyChangeEvent evt) {
188: if (evt.getPropertyName() == null
189: || PROP_DOCUMENT.equals(evt.getPropertyName())) {
190: rebuildAll();
191: }
192:
193: if (PROP_HL_INCLUDES.equals(evt.getPropertyName())
194: || PROP_HL_EXCLUDES.equals(evt.getPropertyName())) {
195: synchronized (this ) {
196: paneFilter = new RegExpFilter(pane
197: .getClientProperty(PROP_HL_INCLUDES), pane
198: .getClientProperty(PROP_HL_EXCLUDES));
199: rebuildAllContainers(pane.getDocument());
200: }
201: }
202: }
203:
204: // ----------------------------------------------------------------------
205: // Private implementation
206: // ----------------------------------------------------------------------
207:
208: private MimePath[] getAllDocumentMimePath() {
209: Document doc = pane.getDocument();
210: String mainMimeType;
211:
212: Object propMimeType = doc.getProperty(PROP_MIME_TYPE);
213: if (propMimeType != null) {
214: mainMimeType = propMimeType.toString();
215: } else {
216: mainMimeType = pane.getUI().getEditorKit(pane)
217: .getContentType();
218: }
219:
220: return new MimePath[] { MimePath.parse(mainMimeType) };
221: }
222:
223: private synchronized void rebuildAll() {
224: // Get the new set of mime path
225: MimePath[] mimePaths = getAllDocumentMimePath();
226:
227: // Recalculate factories and all containers if needed
228: if (!Utilities.compareObjects(lastKnownDocument, pane
229: .getDocument())
230: || !Arrays.equals(lastKnownMimePaths, mimePaths)) {
231: if (LOG.isLoggable(Level.FINE)) {
232: LOG.fine("rebuildAll: lastKnownDocument = "
233: + simpleToString(lastKnownDocument)
234: + //NOI18N
235: ", document = "
236: + simpleToString(pane.getDocument())
237: + //NOI18N
238: ", lastKnownMimePaths = "
239: + mimePathsToString(lastKnownMimePaths)
240: + //NOI18N
241: ", mimePaths = "
242: + mimePathsToString(mimePaths)); //NOI18N
243: }
244:
245: // Unregister listeners
246: if (factories != null) {
247: factories.removeLookupListener(factoriesTracker);
248: }
249: if (settings != null) {
250: settings.removeLookupListener(settingsTracker);
251: }
252:
253: if (mimePaths != null) {
254: ArrayList<Lookup> lookups = new ArrayList<Lookup>();
255: for (MimePath mimePath : mimePaths) {
256: lookups.add(MimeLookup.getLookup(mimePath));
257: }
258:
259: ProxyLookup lookup = new ProxyLookup(lookups
260: .toArray(new Lookup[lookups.size()]));
261: factories = lookup
262: .lookup(new Lookup.Template<HighlightsLayerFactory>(
263: HighlightsLayerFactory.class));
264: settings = lookup
265: .lookup(new Lookup.Template<FontColorSettings>(
266: FontColorSettings.class));
267: } else {
268: factories = null;
269: settings = null;
270: }
271:
272: // Start listening again
273: if (factories != null) {
274: factories.addLookupListener(factoriesTracker);
275: factories.allItems(); // otherwise we won't get any events at all
276: }
277: if (settings != null) {
278: settings.addLookupListener(settingsTracker);
279: settings.allItems(); // otherwise we won't get any events at all
280: }
281:
282: lastKnownDocument = pane.getDocument();
283: lastKnownMimePaths = mimePaths;
284:
285: rebuildAllLayers();
286: }
287: }
288:
289: private synchronized void resetAllContainers() {
290: for (HighlightsLayerFilter filter : containers.keySet()) {
291: WeakReference<CompoundHighlightsContainer> ref = containers
292: .get(filter);
293: CompoundHighlightsContainer container = ref == null ? null
294: : ref.get();
295:
296: if (container != null) {
297: container.resetCache();
298: }
299: }
300: }
301:
302: private synchronized void rebuildAllLayers() {
303: if (inRebuildAllLayers) {
304: return;
305: }
306:
307: inRebuildAllLayers = true;
308: try {
309: Document doc = pane.getDocument();
310: if (factories != null) {
311: Collection<? extends HighlightsLayerFactory> all = factories
312: .allInstances();
313: HashMap<String, HighlightsLayer> layers = new HashMap<String, HighlightsLayer>();
314:
315: HighlightsLayerFactory.Context context = HighlightingSpiPackageAccessor
316: .get().createFactoryContext(doc, pane);
317:
318: for (HighlightsLayerFactory factory : all) {
319: HighlightsLayer[] factoryLayers = factory
320: .createLayers(context);
321: if (factoryLayers == null) {
322: continue;
323: }
324:
325: for (HighlightsLayer layer : factoryLayers) {
326: HighlightsLayerAccessor layerAccessor = HighlightingSpiPackageAccessor
327: .get().getHighlightsLayerAccessor(
328: layer);
329:
330: String layerTypeId = layerAccessor
331: .getLayerTypeId();
332: if (!layers.containsKey(layerTypeId)) {
333: layers.put(layerTypeId, layer);
334: }
335: }
336: }
337:
338: // Sort the layers by their z-order
339: List<? extends HighlightsLayer> sortedLayers;
340: try {
341: sortedLayers = HighlightingSpiPackageAccessor
342: .get().sort(layers.values());
343: } catch (TopologicalSortException tse) {
344: ErrorManager.getDefault().notify(tse);
345: @SuppressWarnings("unchecked")
346: //NOI18N
347: List<? extends HighlightsLayer> sl = (List<? extends HighlightsLayer>) tse
348: .partialSort();
349: sortedLayers = sl;
350: }
351:
352: // Get the containers
353: ArrayList<HighlightsContainer> layerContainers = new ArrayList<HighlightsContainer>();
354: for (HighlightsLayer layer : sortedLayers) {
355: HighlightsLayerAccessor layerAccessor = HighlightingSpiPackageAccessor
356: .get()
357: .getHighlightsLayerAccessor(layer);
358:
359: layerContainers.add(layerAccessor
360: .getContainer());
361: }
362:
363: allLayers = sortedLayers;
364: allLayerContainers = layerContainers;
365: } else {
366: allLayers = null;
367: allLayerContainers = null;
368: }
369:
370: rebuildAllContainers(doc);
371: } finally {
372: inRebuildAllLayers = false;
373: }
374: }
375:
376: private synchronized void rebuildAllContainers(Document document) {
377: if (LOG.isLoggable(Level.FINE)) {
378: LOG.fine("rebuildAllContainers: lastKnownDocument = "
379: + simpleToString(lastKnownDocument)
380: + //NOI18N
381: ", lastKnownMimePaths = "
382: + mimePathsToString(lastKnownMimePaths)); //NOI18N
383: }
384:
385: for (HighlightsLayerFilter filter : containers.keySet()) {
386: WeakReference<CompoundHighlightsContainer> ref = containers
387: .get(filter);
388: CompoundHighlightsContainer container = ref == null ? null
389: : ref.get();
390:
391: if (container != null) {
392: rebuildContainer(document, filter, container);
393: }
394: }
395: }
396:
397: private synchronized void rebuildContainer(Document doc,
398: HighlightsLayerFilter filter,
399: CompoundHighlightsContainer container) {
400: if (allLayers != null) {
401: List<? extends HighlightsLayer> filteredLayers = paneFilter
402: .filterLayers(Collections
403: .unmodifiableList(allLayers));
404: filteredLayers = filter.filterLayers(Collections
405: .unmodifiableList(filteredLayers));
406:
407: // Get the containers
408: ArrayList<HighlightsContainer> hcs = new ArrayList<HighlightsContainer>();
409: for (HighlightsLayer layer : filteredLayers) {
410: int idx = allLayers.indexOf(layer);
411: HighlightsContainer c = allLayerContainers.get(idx);
412: hcs.add(c);
413: }
414:
415: if (LOG.isLoggable(Level.FINEST)) {
416: logLayers(pane.getDocument(), lastKnownMimePaths,
417: filteredLayers, Level.FINEST);
418: }
419:
420: container.setLayers(doc, hcs
421: .toArray(new HighlightsContainer[hcs.size()]));
422: } else {
423: container.setLayers(null, null);
424: }
425: }
426:
427: private static void logLayers(Document doc,
428: MimePath[] mimePaths,
429: List<? extends HighlightsLayer> layers, Level logLevel) {
430: StringBuilder sb = new StringBuilder();
431:
432: sb.append("HighlighsLayers {\n"); //NOI18N
433: sb.append(" * document : "); //NOI18N
434: sb.append(doc.toString());
435: sb.append("\n"); //NOI18N
436:
437: sb.append(" * mime paths : \n"); //NOI18N
438: for (MimePath mimePath : mimePaths) {
439: sb.append(" "); //NOI18N
440: sb.append(mimePath.getPath());
441: sb.append("\n"); //NOI18N
442: }
443:
444: sb.append(" * layers : \n"); //NOI18N
445: for (HighlightsLayer layer : layers) {
446: HighlightsLayerAccessor layerAccessor = HighlightingSpiPackageAccessor
447: .get().getHighlightsLayerAccessor(layer);
448:
449: sb.append(" "); //NOI18N
450: sb.append(layerAccessor.getLayerTypeId());
451: sb.append('['); //NOI18N
452: sb.append(layerAccessor.getZOrder().toString()); //NOI18N
453: sb.append(']'); //NOI18N
454: sb.append('@'); //NOI18N
455: sb.append(Integer.toHexString(System
456: .identityHashCode(layer)));
457: sb.append("\n"); //NOI18N
458: }
459:
460: sb.append("}\n"); //NOI18N
461:
462: LOG.log(logLevel, sb.toString());
463: }
464:
465: } // End of Highlighting class
466:
467: private static final class RegExpFilter implements
468: HighlightsLayerFilter {
469:
470: private final List<Pattern> includes;
471: private final List<Pattern> excludes;
472:
473: public RegExpFilter(Object includes, Object excludes) {
474: this .includes = buildPatterns(includes);
475: this .excludes = buildPatterns(excludes);
476: }
477:
478: public List<? extends HighlightsLayer> filterLayers(
479: List<? extends HighlightsLayer> layers) {
480: List<? extends HighlightsLayer> includedLayers;
481:
482: if (includes.isEmpty()) {
483: includedLayers = layers;
484: } else {
485: includedLayers = filter(layers, includes, true);
486: }
487:
488: List<? extends HighlightsLayer> filteredLayers;
489: if (excludes.isEmpty()) {
490: filteredLayers = includedLayers;
491: } else {
492: filteredLayers = filter(includedLayers, excludes, false);
493: }
494:
495: return filteredLayers;
496: }
497:
498: private static List<? extends HighlightsLayer> filter(
499: List<? extends HighlightsLayer> layers,
500: List<Pattern> patterns, boolean includeMatches // true means include matching layers, false means include non-matching layers
501: ) {
502: List<HighlightsLayer> filtered = new ArrayList<HighlightsLayer>();
503:
504: for (HighlightsLayer layer : layers) {
505: HighlightsLayerAccessor layerAccessor = HighlightingSpiPackageAccessor
506: .get().getHighlightsLayerAccessor(layer);
507:
508: for (Pattern pattern : patterns) {
509: boolean matches = pattern.matcher(
510: layerAccessor.getLayerTypeId()).matches();
511:
512: if (matches && includeMatches) {
513: filtered.add(layer);
514: }
515:
516: if (!matches && !includeMatches) {
517: filtered.add(layer);
518: }
519: }
520: }
521:
522: return filtered;
523: }
524:
525: private static List<Pattern> buildPatterns(Object expressions) {
526: List<Pattern> patterns = new ArrayList<Pattern>();
527:
528: if (expressions instanceof String) {
529: try {
530: patterns.add(Pattern.compile((String) expressions));
531: } catch (PatternSyntaxException e) {
532: LOG
533: .log(
534: Level.WARNING,
535: "Ignoring invalid regexp for the HighlightsLayer filtering.",
536: e); //NOI18N
537: }
538: } else if (expressions instanceof String[]) {
539: for (String expression : (String[]) expressions) {
540: try {
541: patterns.add(Pattern.compile(expression));
542: } catch (PatternSyntaxException e) {
543: LOG
544: .log(
545: Level.WARNING,
546: "Ignoring invalid regexp for the HighlightsLayer filtering.",
547: e); //NOI18N
548: }
549: }
550: }
551:
552: return patterns;
553: }
554: } // End of RegExpFilter class
555:
556: private static String simpleToString(Object o) {
557: return o == null ? "null" : o.getClass().getName() + "@"
558: + Integer.toHexString(System.identityHashCode(o)); //NOI18N
559: }
560:
561: private static String mimePathsToString(MimePath... mimePaths) {
562: if (mimePaths == null) {
563: return "null";
564: } else {
565: StringBuilder sb = new StringBuilder();
566:
567: sb.append('{'); //NOI18N
568: for (MimePath mp : mimePaths) {
569: sb.append('\'').append(mp.getPath()).append('\''); //NOI18N
570: sb.append(","); //NOI81N
571: }
572: sb.append('}'); //NOI18N
573:
574: return sb.toString();
575: }
576: }
577: }
|