View Javadoc

1   /*
2    * $Id: AttributeFormattingStrategy.java,v 1.10 2005/06/01 14:35:58 jlerner Exp $
3    *
4    * Copyright (c) 1999-2004, BBN Technologies, LLC.
5    * All rights reserved.
6    * http://www.daml.org/legal/opensource/bbn_license.html
7    */
8    
9   package com.bbn.swede.editor.formatting;
10  
11  import java.util.Iterator;
12  import java.util.LinkedList;
13  
14  import org.eclipse.jface.text.BadLocationException;
15  import org.eclipse.jface.text.IDocument;
16  import org.eclipse.jface.text.IRegion;
17  import org.eclipse.jface.text.ITypedRegion;
18  import org.eclipse.jface.text.Region;
19  import org.eclipse.jface.text.TypedPosition;
20  import org.eclipse.jface.text.TypedRegion;
21  import org.eclipse.jface.text.formatter.FormattingContextProperties;
22  import org.eclipse.jface.text.formatter.IFormattingContext;
23  import org.eclipse.jface.text.formatter.IFormattingStrategy;
24  import org.eclipse.jface.text.formatter.IFormattingStrategyExtension;
25  import org.eclipse.text.edits.MalformedTreeException;
26  import org.eclipse.text.edits.MultiTextEdit;
27  import org.eclipse.text.edits.ReplaceEdit;
28  import org.eclipse.text.edits.TextEdit;
29  
30  import com.bbn.swede.core.OWLCore;
31  import com.bbn.swede.core.dom.AttributeNode;
32  import com.bbn.swede.core.dom.IOWLAbstractSyntaxTree;
33  import com.bbn.swede.core.dom.OASTNode;
34  import com.bbn.swede.core.dom.TagNode;
35  import com.bbn.swede.editor.EditorPlugin;
36  
37  /***
38   * A slave strategy for formatting begin tags in OWL documents.  This strategy
39   * is responsible for splitting attributes across multiple lines, aligning
40   * attribute values, and substituting attribute delimeters.
41   * @author jlerner
42   */
43  public class AttributeFormattingStrategy implements IFormattingStrategy, 
44     IFormattingStrategyExtension
45  {
46     private LinkedList _lRegions = new LinkedList();
47     private LinkedList _lDocuments = new LinkedList();
48     private LinkedList _lPartitions = new LinkedList();
49     private IOWLAbstractSyntaxTree _oast;
50     
51     /***
52      * Creates an attribute formatting strategy.
53      * @param oast The abstract syntaxt tree for the OWL document this formatter
54      *             will run against.
55      */
56     public AttributeFormattingStrategy(IOWLAbstractSyntaxTree oast)
57     {
58        _oast = oast;
59     }
60  
61     /* (non-Javadoc)
62      * @see org.eclipse.jface.text.formatter.IFormattingStrategy#formatterStarts(java.lang.String)
63      */
64     public void formatterStarts(String initialIndentation)
65     {
66     }
67  
68     /* (non-Javadoc)
69      * @see org.eclipse.jface.text.formatter.IFormattingStrategy#format(
70      *    java.lang.String, boolean, java.lang.String, int[])
71      */
72     public String format(String content, boolean isLineStart, String indentation, int[] positions)
73     {
74        return null;
75     }
76  
77     /* (non-Javadoc)
78      * @see org.eclipse.jface.text.formatter.IFormattingStrategy#formatterStops()
79      */
80     public void formatterStops()
81     {
82        _lRegions.clear();
83        _lDocuments.clear();
84        _lPartitions.clear();
85     }
86  
87     /* (non-Javadoc)
88      * @see org.eclipse.jface.text.formatter.IFormattingStrategyExtension#format()
89      */
90     public void format()
91     {
92        IDocument document = (IDocument)_lDocuments.removeFirst();
93        IRegion region = (IRegion)_lRegions.removeFirst();
94        TypedPosition position = (TypedPosition)_lPartitions.removeFirst();
95        if (document == null || region == null)
96        {
97           return;
98        }
99        
100       
101       try
102       {
103          //This will always be a complete tag, since the multi-pass formatter
104          //only deals with regions completely enclosed in the selection.
105          ITypedRegion partition = new TypedRegion(position.getOffset(), position.getLength(), position.getType());
106          TextEdit edit = wrapAttributes(document, partition);
107          if (edit != null)
108          {
109             edit.apply(document);
110          }
111       }
112       catch (MalformedTreeException e)
113       {
114          OWLCore.logWarning(EditorPlugin.getID(), "Unable to format attributes", e);
115       }
116       catch (BadLocationException e)
117       {
118          OWLCore.logWarning(EditorPlugin.getID(), "Unable to format attributes", e);
119       }   
120    }
121    
122    /***
123     * Determines the length of the longest attribute name in a tag. 
124     * @param sTag The text of the tag
125     * @param iBaseOffset The offset of the tag string within the document
126     * @param lRegions A list of IRegions specifying the locations of the tag's
127     *                 attributes.
128     * @return The length of the longest attribute name in the tag.
129     */
130    private int calculateMaxQNameLength(String sTag, int iBaseOffset, LinkedList lRegions)
131    {
132       //build offset list off of regions
133       LinkedList lOffsets = new LinkedList();
134       //ignore first and last entries, which don't represent attributes.
135       for (int i = 1; i < lRegions.size() - 1; i++)
136       {
137          IRegion region = (IRegion)lRegions.get(i);
138          lOffsets.addLast(new Integer(region.getOffset() - iBaseOffset));
139       }
140       
141       int iWidth = 0;
142       Iterator it = lOffsets.iterator();
143       while (it.hasNext())
144       {
145          Integer i = (Integer)it.next();
146          int iPos = sTag.indexOf('=', i.intValue());
147          if (iPos < 0)
148          {
149             return -1;
150          }
151          iWidth = Math.max(iWidth, iPos - i.intValue());
152       }
153       return iWidth;
154    }
155    
156 
157    /***
158     * Divides a tag into regions based on its attributes.
159     * @param partition The region containing the tag.  Should match the begin
160     *                  offset of a node in the 
161     * @return A list of IRegions.  The list will always contain at least two
162     *         entries.  The first specifies the opening angle bracket and the
163     *         tag's qname.  The last specifies the closing angle bracket or, if
164     *         the tag is a singleton, />.  Each region between these in the list
165     *         specifies an attribute of the tag.
166     *         This method will return <code>null</code> if the text to be
167     *         formatted does not exist as a tag node within the tree.  Callers
168     *         should check for this to avoid formatting unparseables or other
169     *         text nodes.
170     */
171    private LinkedList partitionTag(ITypedRegion partition)
172    {
173       OASTNode node = _oast.getNode(partition.getOffset());
174       if (node == null || !(node instanceof TagNode) 
175           || node.getOffset() != partition.getOffset())
176       {
177          return null;
178       }
179       
180       LinkedList ll = new LinkedList();
181       
182       IRegion region = new Region(partition.getOffset(), node.getQName().length() + 1);
183       ll.addLast(region);
184       
185       AttributeNode[] attributes = ((TagNode)node).getAttributes();
186       for (int i = 0; i < attributes.length; i++)
187       {
188          AttributeNode att = attributes[i];
189          region = new Region(att.getOffset(), att.getLength());
190          ll.addLast(region);
191       }
192       
193       if (node.getLength() == partition.getLength())
194       {
195          //singleton tag
196          region = new Region(partition.getOffset() + partition.getLength() - 2, 2);
197       }
198       else
199       {
200          //regular begin tag
201          region = new Region(partition.getOffset() + partition.getLength() - 1, 1);
202       }
203       ll.addLast(region);
204       
205       return ll;
206    }
207    
208    /***
209     * Isolates the text for a single attribute from the full text of the tag.
210     * @param sTag The full text of the tag
211     * @param offsetTag The tag's offset in the document
212     * @param offsetAtt The desired attribute's offset in the document
213     * @param lengthAtt The length of the desired attribute, in characters 
214     * @return The text of the specified attribute
215     */
216    private String getAttributeText(String sTag, int offsetTag, int offsetAtt, int lengthAtt)
217    {
218       int iStart = offsetAtt - offsetTag;
219       int iEnd = iStart + lengthAtt;
220       return sTag.substring(iStart, iEnd);
221    }
222    
223    /***
224     * Adjust an attribute's text to reflect delimiter and alignment preferences.
225     * @param sAtt The full text of the attribute
226     * @param offsetAtt The attribute's offset within the document
227     * @param width The qname width to match for alignment of attribute values,
228     *              or -1 if values are not being aligned.
229     * @return The modified text of the attribute, or <code>null</code> if no
230     *         changes are necessary.
231     */
232    private String sanitizeAttribute(String sAtt, int offsetAtt, int width)
233    {
234       StringBuffer sb = new StringBuffer(sAtt);
235       if (width >= 0)
236       {
237          int iPos = sb.indexOf("=");
238          if (iPos >= 0 && iPos != width)
239          {
240             int i;
241             for (i = iPos; i > 0 && Character.isWhitespace(sAtt.charAt(i - 1)); i--)
242             {
243                //do nothing
244             }
245             String sSpaces = FormattingUtils.getIndentSpaces(width - i);
246             sb.replace(i, iPos, sSpaces);
247          }
248       }
249       
250       String sDelim = EditorPlugin.getAttributeDelimeter();
251       char cDelim = sDelim.charAt(0);
252       int iSingle = sb.indexOf("'");
253       int iDouble = sb.indexOf("\"");
254       int iFirst = -1;
255       int iLast = -1;
256       if (iSingle >= 0 && iSingle < iDouble)
257       {
258          iFirst = iSingle;
259          iLast = sb.lastIndexOf("'");
260       }
261       else if (iDouble >= 0)
262       {
263          iFirst = iDouble;
264          iLast = sb.lastIndexOf("\"");
265       }
266       if (iFirst >= 0 && iLast >= 0)
267       {
268          sb.replace(iFirst, iFirst + 1, sDelim);
269          sb.replace(iLast, iLast + 1, sDelim);
270          
271          //Do an entity substitution for any occurrances of the delimieter within
272          //the attribute value
273          String sSub = (cDelim == '\'' ? "&#39;" : "&quot;");
274          int iPos = iFirst + 1;
275          iPos = sb.indexOf(sDelim, iPos);
276          while (iPos > iFirst && iPos < iLast)
277          {
278             sb.replace(iPos, iPos + 1, sSub);
279             iPos = sb.indexOf(sDelim, iPos);
280             iLast += sSub.length() - 1;
281          }
282       }
283 
284       //Make sure there were actually some changes before returning a text edit
285       if (sb.equals(sAtt))
286       {
287          return null;
288       }
289 
290       return sb.toString();
291    }
292    
293    private TextEdit wrapAttributes(IDocument document, ITypedRegion partition)
294       throws BadLocationException
295    {
296       String sTag = FormattingUtils.getPartitionText(document, partition);
297       int iBaseIndent = FormattingUtils.getIndentLength(document, partition);
298       boolean bWrap = EditorPlugin.splitAttributes(iBaseIndent + sTag.length());
299       
300       LinkedList lRegions = partitionTag(partition);
301       if (lRegions == null)
302       {
303          return null;
304       }
305       
306       boolean bAlignValues = bWrap && EditorPlugin.alignAttributeValues(); 
307       int iWidth = -1;
308       if (bAlignValues)
309       {
310          iWidth = calculateMaxQNameLength(sTag, partition.getOffset(), lRegions);
311          if (iWidth < 0)
312          {
313             bAlignValues = false;
314          }
315       }
316       MultiTextEdit edit = new MultiTextEdit(partition.getOffset(), partition.getLength());
317       int iIndent = FormattingUtils.getAttributeIndentSize();
318       if (EditorPlugin.alignAttributes())
319       {
320          //Pull off the qname region and ensure there's a space between it and
321          //the first attribute.
322          IRegion rQName = (IRegion)lRegions.removeFirst();
323          IRegion rAttribute = (IRegion)lRegions.get(0);
324          int iEnd = rQName.getOffset() + rQName.getLength();
325          TextEdit subEdit = new ReplaceEdit(iEnd, rAttribute.getOffset() - iEnd, " ");
326          edit.addChild(subEdit);
327          //Remove the first attribute offset so it doesn't get wrapped,
328          //and use it as the indent level for remaining attributes
329          iIndent = rQName.getLength() + 1;
330       }
331       iIndent += iBaseIndent;
332       String sIndent = FormattingUtils.getIndent(iIndent);
333       
334       //Wrap attributes and sanitize delimiters
335       String sReplace = (bWrap ? "\n" + sIndent : " ");
336       boolean bWrapEnd = false;
337       while (lRegions.size() > 2)
338       {
339          IRegion regionLast = (IRegion)lRegions.removeFirst();
340          IRegion regionNext = (IRegion)lRegions.get(0);
341          int iEnd = regionLast.getOffset() + regionLast.getLength();
342          String sSanitized = sanitizeAttribute(
343             getAttributeText(
344                sTag, 
345                partition.getOffset(), 
346                regionNext.getOffset(), 
347                regionNext.getLength()),
348             regionLast.getOffset(),
349             iWidth);
350          if (sSanitized == null)
351          {
352             edit.addChild(new ReplaceEdit(iEnd, regionNext.getOffset() - iEnd, sReplace));
353          }
354          else
355          {
356             edit.addChild(
357                new ReplaceEdit(
358                   iEnd, 
359                   regionNext.getOffset() - iEnd + regionNext.getLength(),
360                   sReplace + sSanitized));
361          }
362          bWrapEnd = bWrap;
363       }
364 
365       if (bWrapEnd)
366       {
367          //at least one attribute was wrapped, so we must also wrap the end of
368          //the tag.
369          IRegion regionLast = (IRegion)lRegions.removeFirst();
370          IRegion regionNext = (IRegion)lRegions.get(0);
371          int iEnd = regionLast.getOffset() + regionLast.getLength();
372          sIndent = FormattingUtils.getIndent(iBaseIndent);
373          edit.addChild(new ReplaceEdit(iEnd, regionNext.getOffset() - iEnd, "\n" + sIndent));
374       }
375       return edit;
376    }
377 
378    /* (non-Javadoc)
379     * @see org.eclipse.jface.text.formatter.IFormattingStrategyExtension
380     *    #formatterStarts(org.eclipse.jface.text.formatter.IFormattingContext)
381     */
382    public void formatterStarts(IFormattingContext context)
383    {
384       _lPartitions.addLast(context.getProperty(FormattingContextProperties.CONTEXT_PARTITION));
385       _lRegions.addLast(context.getProperty(FormattingContextProperties.CONTEXT_REGION));
386       _lDocuments.addLast(context.getProperty(FormattingContextProperties.CONTEXT_MEDIUM));
387    }
388 
389 }