Automated merge with http://hg.netbeans.org/main/contrib
authorMartin Fousek <marfous@netbeans.org>
Sun, 17 Jul 2011 01:29:57 +0200
changeset 17525d81178ecf7de
parent 17523 3a011dc2eb95
parent 17524 b160e2055090
child 17526 97bc04d587aa
Automated merge with http://hg.netbeans.org/main/contrib
     1.1 --- a/php.smarty/manifest.mf	Fri Jul 15 09:51:22 2011 -0400
     1.2 +++ b/php.smarty/manifest.mf	Sun Jul 17 01:29:57 2011 +0200
     1.3 @@ -2,5 +2,5 @@
     1.4  OpenIDE-Module: org.netbeans.modules.php.smarty
     1.5  OpenIDE-Module-Layer: org/netbeans/modules/php/smarty/resources/layer.xml
     1.6  OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/php/smarty/resources/Bundle.properties
     1.7 -OpenIDE-Module-Specification-Version: 1.55
     1.8 +OpenIDE-Module-Specification-Version: 1.56
     1.9  
     2.1 --- a/php.smarty/src/org/netbeans/modules/php/smarty/editor/Bundle.properties	Fri Jul 15 09:51:22 2011 -0400
     2.2 +++ b/php.smarty/src/org/netbeans/modules/php/smarty/editor/Bundle.properties	Sun Jul 17 01:29:57 2011 +0200
     2.3 @@ -37,3 +37,21 @@
     2.4  
     2.5  #TplDataLoader
     2.6  PROP_TplLoader_Name=TPL Objects
     2.7 +
     2.8 +# TplEditorSupport
     2.9 +# {0} document name
    2.10 +# {1} encoding
    2.11 +# {2} original encoding of the file when loaded
    2.12 +# {3} "the original" or empty string depending on context
    2.13 +MSG_unsupportedEncodingSave=<html>The encoding {1} specified in meta tag of the document {0} is invalid<br> or the document contains characters which cannot be saved using this encoding.<br> Do you want to save the file using{3} <b>{2}</b> encoding?</html>
    2.14 +
    2.15 +# TplEditorSupport
    2.16 +# {0} document name
    2.17 +# {1} encoding
    2.18 +# {2} alternative encoding
    2.19 +MSG_unsupportedEncodingLoad=<html>The encoding {1} specified in meta tag of the document {0} is invalid.<br>Do you want to load the file using <b>{2}</b> encoding?</html>
    2.20 +
    2.21 +# TplEditorSupport
    2.22 +# {0} = name of file
    2.23 +# {1} = the bad encoding
    2.24 +MSG_badCharConversionSave=<html>The {0} contains characters which will probably be damaged during conversion to the {1} character set.<br> Do you want to save the file using <b>UTF-8</b> character set?</html>
     3.1 --- a/php.smarty/src/org/netbeans/modules/php/smarty/editor/TplDataObject.java	Fri Jul 15 09:51:22 2011 -0400
     3.2 +++ b/php.smarty/src/org/netbeans/modules/php/smarty/editor/TplDataObject.java	Sun Jul 17 01:29:57 2011 +0200
     3.3 @@ -4,8 +4,32 @@
     3.4   */
     3.5  package org.netbeans.modules.php.smarty.editor;
     3.6  
     3.7 +import java.io.ByteArrayInputStream;
     3.8  import java.io.IOException;
     3.9 +import java.io.InputStream;
    3.10 +import java.io.InputStreamReader;
    3.11 +import java.io.UnsupportedEncodingException;
    3.12 +import java.nio.ByteBuffer;
    3.13 +import java.nio.CharBuffer;
    3.14 +import java.nio.charset.Charset;
    3.15 +import java.nio.charset.CharsetDecoder;
    3.16 +import java.nio.charset.CharsetEncoder;
    3.17 +import java.nio.charset.CoderResult;
    3.18 +import java.nio.charset.IllegalCharsetNameException;
    3.19 +import java.nio.charset.UnsupportedCharsetException;
    3.20 +import java.util.concurrent.atomic.AtomicBoolean;
    3.21 +import java.util.logging.Level;
    3.22 +import java.util.logging.Logger;
    3.23 +import org.netbeans.api.html.lexer.HTMLTokenId;
    3.24 +import org.netbeans.api.lexer.Token;
    3.25 +import org.netbeans.api.lexer.TokenHierarchy;
    3.26 +import org.netbeans.api.lexer.TokenSequence;
    3.27 +import org.netbeans.api.queries.FileEncodingQuery;
    3.28 +import org.netbeans.spi.queries.FileEncodingQueryImplementation;
    3.29 +import org.openide.filesystems.FileChangeAdapter;
    3.30 +import org.openide.filesystems.FileEvent;
    3.31  import org.openide.filesystems.FileObject;
    3.32 +import org.openide.filesystems.FileUtil;
    3.33  import org.openide.loaders.DataNode;
    3.34  import org.openide.loaders.DataObjectExistsException;
    3.35  import org.openide.loaders.MultiDataObject;
    3.36 @@ -14,12 +38,24 @@
    3.37  import org.openide.nodes.CookieSet;
    3.38  import org.openide.nodes.Node;
    3.39  import org.openide.nodes.Children;
    3.40 +import org.openide.util.Exceptions;
    3.41  import org.openide.util.Lookup;
    3.42  import org.openide.util.UserCancelException;
    3.43  
    3.44 +/**
    3.45 + * Most code of this class was get from HtmlDataObject - especially encoding support.
    3.46 + * 
    3.47 + * @author Martin Fousek <marfous@netbeans.org>
    3.48 + */
    3.49  public class TplDataObject extends MultiDataObject implements CookieSet.Factory {
    3.50  
    3.51      private transient TplEditorSupport tplEditorSupport;
    3.52 +    
    3.53 +    public static final String DEFAULT_ENCODING = new InputStreamReader(System.in).getEncoding();
    3.54 +    //constants used when finding tpl (html) document content type
    3.55 +    private static final String CHARSET_DECL = "CHARSET="; //NOI18N
    3.56 +    
    3.57 +    private static final Logger LOG = Logger.getLogger(TplDataObject.class.getName());
    3.58  
    3.59      public TplDataObject(FileObject pf, MultiFileLoader loader) throws DataObjectExistsException, IOException {
    3.60          super(pf, loader);
    3.61 @@ -31,12 +67,15 @@
    3.62              public void saveAs( FileObject folder, String fileName ) throws IOException {
    3.63                  TplEditorSupport es = getCookie( TplEditorSupport.class );
    3.64                  try {
    3.65 +                    es.updateEncoding();
    3.66                      es.saveAs( folder, fileName );
    3.67                  } catch (UserCancelException e) {
    3.68                      //ignore, just not save anything
    3.69                  }
    3.70              }
    3.71          });
    3.72 +        
    3.73 +        set.assign(FileEncodingQueryImplementation.class, new FileEncodingQueryImpl());
    3.74      }
    3.75  
    3.76      @Override
    3.77 @@ -54,21 +93,454 @@
    3.78          /** Creates new Cookie */
    3.79      public Node.Cookie createCookie(Class klass) {
    3.80          if (klass.isAssignableFrom(TplEditorSupport.class)) {
    3.81 -            return getHtmlEditorSupport();
    3.82 +            return getTplEditorSupport();
    3.83          } else {
    3.84              return null;
    3.85          }
    3.86      }
    3.87  
    3.88 -    private synchronized TplEditorSupport getHtmlEditorSupport() {
    3.89 +    private synchronized TplEditorSupport getTplEditorSupport() {
    3.90          if (tplEditorSupport == null) {
    3.91              tplEditorSupport = new TplEditorSupport(this);
    3.92          }
    3.93          return tplEditorSupport;
    3.94      }
    3.95 -
    3.96 +    
    3.97      // Package accessibility for TplEditorSupport:
    3.98 -    public CookieSet getCookieSet0() {
    3.99 +    CookieSet getCookieSet0() {
   3.100          return getCookieSet();
   3.101      }
   3.102 +
   3.103 +    /** Checks the file for UTF-16 marks and calls findEncoding with properly loaded document content then. */
   3.104 +    String getFileEncoding() {
   3.105 +        InputStream is = null;
   3.106 +        try {
   3.107 +            FileObject pf = getPrimaryFile();
   3.108 +            if(!pf.isValid()) {
   3.109 +                return null;
   3.110 +            }
   3.111 +            is = pf.getInputStream();
   3.112 +            return getFileEncoding(is);
   3.113 +        } catch (IOException ex) {
   3.114 +            LOG.log(Level.WARNING, null, ex);
   3.115 +        } finally {
   3.116 +            try {
   3.117 +                if (is != null) {
   3.118 +                    is.close();
   3.119 +                }
   3.120 +            } catch (IOException ex) {
   3.121 +                LOG.log(Level.WARNING, null, ex);
   3.122 +            }
   3.123 +        }
   3.124 +        return null;
   3.125 +    }
   3.126 +
   3.127 +    private String getFileEncoding(final InputStream in) throws IOException {
   3.128 +        //detect encoding from input stream
   3.129 +        String encoding = null;
   3.130 +            byte[] arr = new byte[4096];
   3.131 +        int len = in.read(arr);
   3.132 +            len = (len >= 0) ? len : 0;
   3.133 +            //check UTF-16 mark
   3.134 +            if (len > 1) {
   3.135 +                int mark = (arr[0]&0xff)*0x100+(arr[1]&0xff);
   3.136 +                if (mark == 0xfeff) {
   3.137 +                    encoding = "UTF-16"; // NOI18N
   3.138 +                } else if(mark == 0xfffe) {
   3.139 +                    encoding = "UTF-16LE";
   3.140 +                }
   3.141 +            }
   3.142 +            //try to read the file using some encodings
   3.143 +            String[] encodings = new String[]{encoding != null ? encoding : DEFAULT_ENCODING, "UTF-16LE", "UTF-16BE"};
   3.144 +            int i = 0;
   3.145 +            do {
   3.146 +                encoding = findEncoding(makeString(arr, 0, len, encodings[i++]));
   3.147 +            } while (encoding == null && i < encodings.length);
   3.148 +            
   3.149 +        if (encoding != null) {
   3.150 +            encoding = encoding.trim();
   3.151 +        }
   3.152 +        return encoding;
   3.153 +    }
   3.154 +    
   3.155 +    private String makeString(byte[] arr, int offset, int len, String encoding) throws UnsupportedEncodingException {
   3.156 +        return new String(arr, 0, len, encoding).toUpperCase();
   3.157 +    }
   3.158 +    
   3.159 +        
   3.160 +    /** Tries to guess the mime type from given input stream. Tries to find
   3.161 +     *   <em>&lt;meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"&gt;</em>
   3.162 +     * @param txt the string to search in (should be in upper case)
   3.163 +     * @return the encoding or null if no has been found
   3.164 +     */
   3.165 +    static String findEncoding(String txt) {
   3.166 +        int[] offsets = findEncodingOffsets(txt);
   3.167 +        if (offsets.length == 3) {
   3.168 +            String encoding = txt.substring(offsets[0] + offsets[1], offsets[0] + offsets[2]);
   3.169 +            return encoding;
   3.170 +        }
   3.171 +        return null;
   3.172 +    }
   3.173 +    
   3.174 +    private static int[] findEncodingOffsets(String txt) {
   3.175 +        int[] rslt = new int[0];
   3.176 +        TokenHierarchy hi = TokenHierarchy.create(txt, HTMLTokenId.language());
   3.177 +        TokenSequence ts = hi.tokenSequence();
   3.178 +        ts.moveStart();
   3.179 +        while(ts.moveNext()) {
   3.180 +            Token token = ts.token();
   3.181 +            if(token.id() == HTMLTokenId.VALUE) {
   3.182 +                String tokenImage = token.text().toString();
   3.183 +                int charsetOffset = tokenImage.indexOf(CHARSET_DECL);
   3.184 +                charsetOffset = charsetOffset == -1 ? tokenImage.indexOf(CHARSET_DECL.toLowerCase()) : charsetOffset;
   3.185 +                
   3.186 +                int charsetEndOffset = charsetOffset + CHARSET_DECL.length();
   3.187 +                if (charsetOffset != -1){
   3.188 +                    int endOffset = tokenImage.indexOf('"', charsetEndOffset);
   3.189 +                    
   3.190 +                    if (endOffset == -1){
   3.191 +                        endOffset = tokenImage.indexOf('\'', charsetEndOffset);
   3.192 +                    }
   3.193 +                    
   3.194 +                    if (endOffset == -1){
   3.195 +                        endOffset = tokenImage.indexOf(';', charsetEndOffset);
   3.196 +                    }
   3.197 +                    
   3.198 +                    if (endOffset == -1){
   3.199 +                        return rslt;
   3.200 +                    }
   3.201 +                    
   3.202 +                    rslt =  new int[]{token.offset(hi), charsetEndOffset, endOffset};
   3.203 +                }
   3.204 +            }
   3.205 +        }
   3.206 +        return rslt;
   3.207 +    }
   3.208 +    
   3.209 +    private class FileEncodingQueryImpl extends FileEncodingQueryImplementation {
   3.210 +
   3.211 +        private volatile Charset cachedEncoding;
   3.212 +        private final AtomicBoolean listeningOnContentChange = new AtomicBoolean();
   3.213 +        private final ThreadLocal<Boolean> callingFEQ = new ThreadLocal<Boolean>() {
   3.214 +            @Override
   3.215 +            protected Boolean initialValue() {
   3.216 +                return false;
   3.217 +            }
   3.218 +        };
   3.219 +
   3.220 +        @Override
   3.221 +        public Charset getEncoding(FileObject file) {
   3.222 +            assert file != null;
   3.223 +            if(callingFEQ.get()) {
   3.224 +                //we are calling to the FEQ from within this method so
   3.225 +                //we must not return anything to prevent cycling
   3.226 +                return null;
   3.227 +            }
   3.228 +
   3.229 +            Charset encoding = cachedEncoding;
   3.230 +            if (encoding != null) {
   3.231 +                LOG.log(Level.FINEST, "TplDataObject.getFileEncoding cached {0}", new Object[] {encoding});   //NOI18N
   3.232 +                return encoding;
   3.233 +            } else {
   3.234 +                //get the encoding from the FEQ excluding this FEQ implementation
   3.235 +                //so the proxy charset can default to appropriate encoding
   3.236 +                callingFEQ.set(true);
   3.237 +                try {
   3.238 +                    Charset charset = FileEncodingQuery.getEncoding(file);
   3.239 +                    return new ProxyCharset(charset);
   3.240 +                } finally {
   3.241 +                    callingFEQ.set(false);
   3.242 +                }
   3.243 +            }
   3.244 +        }
   3.245 +
   3.246 +        private Charset cache (final Charset encoding) {
   3.247 +
   3.248 +            if (!listeningOnContentChange.getAndSet(true)) {
   3.249 +                final FileObject primaryFile = getPrimaryFile();
   3.250 +                primaryFile.addFileChangeListener(FileUtil.weakFileChangeListener(new FileChangeAdapter(){
   3.251 +                    @Override
   3.252 +                    public void fileChanged(FileEvent fe) {
   3.253 +                        cachedEncoding = null;
   3.254 +                    }
   3.255 +                },primaryFile));
   3.256 +            }
   3.257 +            cachedEncoding = encoding;
   3.258 +            LOG.log(Level.FINEST, "TplDataObject.getFileEncoding noncached {0}", new Object[] {encoding});   //NOI18N
   3.259 +            return encoding;
   3.260 +        }
   3.261 +
   3.262 +
   3.263 +        private class ProxyCharset extends Charset {
   3.264 +
   3.265 +            public ProxyCharset (Charset charset) {
   3.266 +                super (charset.name(), new String[0]);         //NOI18N
   3.267 +            }
   3.268 +            
   3.269 +            public boolean contains(Charset c) {
   3.270 +                return false;
   3.271 +            }
   3.272 +
   3.273 +            public CharsetDecoder newDecoder() {
   3.274 +                return new HtmlDecoder (this);
   3.275 +            }
   3.276 +
   3.277 +            public CharsetEncoder newEncoder() {
   3.278 +                return new HtmlEncoder (this);
   3.279 +            }
   3.280 +        }
   3.281 +
   3.282 +        private class HtmlEncoder extends CharsetEncoder {
   3.283 +
   3.284 +            private CharBuffer buffer = CharBuffer.allocate(4*1024);
   3.285 +            private CharBuffer remainder;
   3.286 +            private CharsetEncoder encoder;
   3.287 +
   3.288 +            public HtmlEncoder (Charset cs) {
   3.289 +                super(cs, 1,2);
   3.290 +            }
   3.291 +
   3.292 +
   3.293 +            protected CoderResult encodeLoop(CharBuffer in, ByteBuffer out) {
   3.294 +                if (buffer == null) {
   3.295 +                    assert encoder != null;
   3.296 +                    if (remainder!=null) {
   3.297 +                        CoderResult result = encoder.encode(remainder,out,false);
   3.298 +                        if (!remainder.hasRemaining()) {
   3.299 +                            remainder = null;
   3.300 +                        }
   3.301 +                    }
   3.302 +                    CoderResult result = encoder.encode(in, out, false);
   3.303 +                    return result;
   3.304 +                }
   3.305 +               if (buffer.remaining() == 0 || (buffer.position() > 0 && in.limit() == 0)) {
   3.306 +                   return handleHead (in,out);
   3.307 +               }
   3.308 +               else if (buffer.remaining() < in.remaining()) {
   3.309 +                   int limit = in.limit();
   3.310 +                   in.limit(in.position()+buffer.remaining());
   3.311 +                   buffer.put(in);
   3.312 +                   in.limit(limit);
   3.313 +                   return handleHead (in, out);
   3.314 +               }
   3.315 +               else {
   3.316 +                   buffer.put(in);
   3.317 +                   return CoderResult.UNDERFLOW;
   3.318 +               }
   3.319 +            }
   3.320 +
   3.321 +            private CoderResult handleHead (CharBuffer in, ByteBuffer out) {
   3.322 +                String encoding = null;
   3.323 +                try {
   3.324 +                    encoding = getEncoding ();
   3.325 +                } catch (IOException ioe) {
   3.326 +                    Exceptions.printStackTrace(ioe);
   3.327 +                }
   3.328 +                if (encoding == null) {
   3.329 +                    buffer = null;
   3.330 +                    throwUnknownEncoding();
   3.331 +                    return null;
   3.332 +                }
   3.333 +                else {
   3.334 +                    Charset c;
   3.335 +                    try {
   3.336 +                        c = cache(Charset.forName(encoding));
   3.337 +                    } catch (UnsupportedCharsetException e) {
   3.338 +                        buffer = null;
   3.339 +                        throwUnknownEncoding();
   3.340 +                        return null;
   3.341 +                    } catch (IllegalCharsetNameException e) {
   3.342 +                        buffer = null;
   3.343 +                        throwUnknownEncoding();
   3.344 +                        return null;
   3.345 +                    }
   3.346 +                    encoder = c.newEncoder();
   3.347 +                    return flushHead(in, out);
   3.348 +                }
   3.349 +            }
   3.350 +
   3.351 +            private CoderResult flushHead (CharBuffer in , ByteBuffer out) {
   3.352 +                buffer.flip();
   3.353 +                CoderResult r = encoder.encode(buffer,out, in==null);
   3.354 +                if (r.isOverflow()) {
   3.355 +                    remainder = buffer;
   3.356 +                    buffer = null;
   3.357 +                    return r;
   3.358 +                }
   3.359 +                else {
   3.360 +                    buffer = null;
   3.361 +                    if (in == null) {
   3.362 +                        return r;
   3.363 +                    }
   3.364 +                    return encoder.encode(in, out, false);
   3.365 +                }
   3.366 +            }
   3.367 +
   3.368 +            private String getEncoding () throws IOException {
   3.369 +                String text = buffer.asReadOnlyBuffer().flip().toString();
   3.370 +                InputStream in = new ByteArrayInputStream(text.getBytes());
   3.371 +                try {
   3.372 +                    return getFileEncoding(in);
   3.373 +                } finally {
   3.374 +                    in.close();
   3.375 +                }
   3.376 +            }
   3.377 +
   3.378 +            @Override
   3.379 +            protected CoderResult implFlush(ByteBuffer out) {
   3.380 +                CoderResult res;
   3.381 +                if (buffer != null) {
   3.382 +                    res = handleHead(null, out);
   3.383 +                    return res;
   3.384 +                }
   3.385 +                else if (remainder != null) {
   3.386 +                    encoder.encode(remainder, out, true);
   3.387 +                }
   3.388 +                else {
   3.389 +                    CharBuffer empty = (CharBuffer) CharBuffer.allocate(0).flip();
   3.390 +                    encoder.encode(empty, out, true);
   3.391 +                }
   3.392 +                res = encoder.flush(out);
   3.393 +                return res;
   3.394 +            }
   3.395 +
   3.396 +            @Override
   3.397 +            protected void implReset() {
   3.398 +                if (encoder != null) {
   3.399 +                    encoder.reset();
   3.400 +                }
   3.401 +            }
   3.402 +        }
   3.403 +
   3.404 +        private class HtmlDecoder extends CharsetDecoder {
   3.405 +
   3.406 +            private ByteBuffer buffer = ByteBuffer.allocate(4*1024);
   3.407 +            private ByteBuffer remainder;
   3.408 +            private CharsetDecoder decoder;
   3.409 +
   3.410 +            public HtmlDecoder (Charset cs) {
   3.411 +                super (cs,1,2);
   3.412 +            }
   3.413 +
   3.414 +            protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) {
   3.415 +                if (buffer == null) {
   3.416 +                    assert decoder != null;
   3.417 +                    if (remainder!=null) {
   3.418 +                        ByteBuffer tmp = ByteBuffer.allocate(remainder.remaining() + in.remaining());
   3.419 +                        tmp.put(remainder);
   3.420 +                        tmp.put(in);
   3.421 +                        tmp.flip();
   3.422 +                        CoderResult result = decoder.decode(tmp,out,false);
   3.423 +                        if (tmp.hasRemaining()) {
   3.424 +                            remainder = tmp;
   3.425 +                        }
   3.426 +                        else {
   3.427 +                            remainder = null;
   3.428 +                        }
   3.429 +                        return result;
   3.430 +                    }
   3.431 +                    else {
   3.432 +                        return decoder.decode(in, out, false);
   3.433 +                    }
   3.434 +               }
   3.435 +               if (buffer.remaining() == 0) {
   3.436 +                   return handleHead (in,out);
   3.437 +               }
   3.438 +               else if (buffer.remaining() < in.remaining()) {
   3.439 +                   int limit = in.limit();
   3.440 +                   in.limit(in.position()+buffer.remaining());
   3.441 +                   buffer.put(in);
   3.442 +                   in.limit(limit);
   3.443 +                   return handleHead (in, out);
   3.444 +               }
   3.445 +               else {
   3.446 +                   buffer.put(in);
   3.447 +                   return CoderResult.UNDERFLOW;
   3.448 +               }
   3.449 +            }
   3.450 +
   3.451 +            private CoderResult handleHead (ByteBuffer in, CharBuffer out) {
   3.452 +                String encoding = null;
   3.453 +                try {
   3.454 +                    encoding = getEncoding ();
   3.455 +                } catch (IOException ioe) {
   3.456 +                    Exceptions.printStackTrace(ioe);
   3.457 +                }
   3.458 +                if (encoding == null) {
   3.459 +                    buffer = null;
   3.460 +                    throwUnknownEncoding();
   3.461 +                    return null;
   3.462 +                }
   3.463 +                else {
   3.464 +                    Charset c;
   3.465 +                    try {
   3.466 +                        c = cache(Charset.forName(encoding));
   3.467 +                    } catch (UnsupportedCharsetException e) {
   3.468 +                        buffer = null;
   3.469 +                        throwUnknownEncoding();
   3.470 +                        return null;
   3.471 +                    } catch (IllegalCharsetNameException e) {
   3.472 +                        buffer = null;
   3.473 +                        throwUnknownEncoding();
   3.474 +                        return null;
   3.475 +                    }
   3.476 +                    decoder = c.newDecoder();
   3.477 +                    return flushHead(in, out);
   3.478 +                }
   3.479 +            }
   3.480 +
   3.481 +            private CoderResult flushHead (ByteBuffer in , CharBuffer out) {
   3.482 +                buffer.flip();
   3.483 +                CoderResult r = decoder.decode(buffer,out, in==null);
   3.484 +                if (r.isOverflow()) {
   3.485 +                    remainder = buffer;
   3.486 +                    buffer = null;
   3.487 +                    return r;
   3.488 +                }
   3.489 +                else {
   3.490 +                    buffer = null;
   3.491 +                    if (in == null) {
   3.492 +                        return r;
   3.493 +                    }
   3.494 +                    return decoder.decode(in, out, false);
   3.495 +                }
   3.496 +            }
   3.497 +
   3.498 +            private String getEncoding () throws IOException {
   3.499 +                byte[] arr = buffer.array();
   3.500 +                ByteArrayInputStream in = new ByteArrayInputStream (arr);
   3.501 +                try {
   3.502 +                    return getFileEncoding(in);
   3.503 +                }
   3.504 +                finally {
   3.505 +                    in.close();
   3.506 +                }
   3.507 +            }
   3.508 +
   3.509 +            @Override
   3.510 +            protected CoderResult implFlush(CharBuffer out) {
   3.511 +                CoderResult res;
   3.512 +                if (buffer != null) {
   3.513 +                    res = handleHead(null, out);
   3.514 +                    return res;
   3.515 +                }
   3.516 +                else if (remainder != null) {
   3.517 +                    decoder.decode(remainder, out, true);
   3.518 +                }
   3.519 +                else {
   3.520 +                    ByteBuffer empty = (ByteBuffer) ByteBuffer.allocate(0).flip();
   3.521 +                    decoder.decode(empty, out, true);
   3.522 +                }
   3.523 +                res = decoder.flush(out);
   3.524 +                return res;
   3.525 +            }
   3.526 +
   3.527 +            @Override
   3.528 +            protected void implReset() {
   3.529 +                if (decoder != null) {
   3.530 +                    decoder.reset();
   3.531 +                }
   3.532 +            }
   3.533 +        }
   3.534 +    }
   3.535  }
     4.1 --- a/php.smarty/src/org/netbeans/modules/php/smarty/editor/TplEditorSupport.java	Fri Jul 15 09:51:22 2011 -0400
     4.2 +++ b/php.smarty/src/org/netbeans/modules/php/smarty/editor/TplEditorSupport.java	Sun Jul 17 01:29:57 2011 +0200
     4.3 @@ -47,9 +47,15 @@
     4.4  import java.io.OutputStreamWriter;
     4.5  import java.io.Writer;
     4.6  import java.nio.charset.Charset;
     4.7 +import java.nio.charset.CharsetEncoder;
     4.8 +import java.util.logging.Level;
     4.9 +import java.util.logging.Logger;
    4.10  import javax.swing.text.BadLocationException;
    4.11  import javax.swing.text.EditorKit;
    4.12  import javax.swing.text.StyledDocument;
    4.13 +import org.netbeans.api.queries.FileEncodingQuery;
    4.14 +import org.openide.DialogDisplayer;
    4.15 +import org.openide.NotifyDescriptor;
    4.16  import org.openide.cookies.EditCookie;
    4.17  import org.openide.cookies.EditorCookie;
    4.18  import org.openide.cookies.OpenCookie;
    4.19 @@ -60,15 +66,23 @@
    4.20  import org.openide.nodes.Node.Cookie;
    4.21  import org.openide.text.CloneableEditor;
    4.22  import org.openide.text.DataEditorSupport;
    4.23 +import org.openide.util.NbBundle;
    4.24  import org.openide.util.UserCancelException;
    4.25  import org.openide.windows.CloneableOpenSupport;
    4.26  
    4.27  /**
    4.28 - * Editor support for TPL data objects.
    4.29 - * @author Martin Fousek
    4.30 + * Editor support for TPL data objects. Most code of this class was get from 
    4.31 + * HtmlEditorSupport - especially encoding support.
    4.32 + * 
    4.33 + * @author Martin Fousek <marfous@netbeans.org>
    4.34   */
    4.35  public final class TplEditorSupport extends DataEditorSupport implements OpenCookie, EditCookie, EditorCookie.Observable, PrintCookie {
    4.36  
    4.37 +    private static final String DOCUMENT_SAVE_ENCODING = "Document_Save_Encoding";
    4.38 +    private static final String UTF_8_ENCODING = "UTF-8";
    4.39 +    // only to be ever user from unit tests:
    4.40 +    public static boolean showConfirmationDialog = true;
    4.41 +    
    4.42      /** SaveCookie for this support instance. The cookie is adding/removing
    4.43       * data object's cookie set depending on if modification flag was set/unset.
    4.44       * It also invokes beforeSave() method on the TplDataObject to give it
    4.45 @@ -100,16 +114,124 @@
    4.46  
    4.47      @Override
    4.48      public void saveDocument() throws IOException {
    4.49 +        updateEncoding();
    4.50          super.saveDocument();
    4.51          TplEditorSupport.this.getDataObject().setModified(false);
    4.52      }
    4.53 +    
    4.54 +    void updateEncoding() throws UserCancelException {
    4.55 +        //try to find encoding specification in the editor content
    4.56 +        String documentContent = getDocumentText();
    4.57 +        String encoding = TplDataObject.findEncoding(documentContent);
    4.58  
    4.59 -    /**
    4.60 +        
    4.61 +        
    4.62 +        
    4.63 +        
    4.64 +        String feqEncoding = FileEncodingQuery.getEncoding(getDataObject().getPrimaryFile()).name();
    4.65 +        String finalEncoding = null;
    4.66 +        if (encoding != null) {
    4.67 +            //found encoding specified in the file content by meta tag
    4.68 +            if (!isSupportedEncoding(encoding) || !canEncode(documentContent, encoding)) {
    4.69 +                //test if the file can be saved by the original encoding or if it needs to be saved using utf-8
    4.70 +                finalEncoding = canEncode(documentContent, feqEncoding) ? feqEncoding : UTF_8_ENCODING;
    4.71 +                NotifyDescriptor nd = new NotifyDescriptor.Confirmation(NbBundle.getMessage(TplEditorSupport.class, "MSG_unsupportedEncodingSave", new Object[]{getDataObject().getPrimaryFile().getNameExt(), encoding, finalEncoding, finalEncoding.equals(UTF_8_ENCODING) ? "" : " the original"}), NotifyDescriptor.YES_NO_OPTION, NotifyDescriptor.WARNING_MESSAGE);
    4.72 +                nd.setValue(NotifyDescriptor.NO_OPTION);
    4.73 +                DialogDisplayer.getDefault().notify(nd);
    4.74 +                if (nd.getValue() != NotifyDescriptor.YES_OPTION) {
    4.75 +                    throw new UserCancelException();
    4.76 +                }
    4.77 +            } else {
    4.78 +                finalEncoding = encoding;
    4.79 +            }
    4.80 +        } else {
    4.81 +            //no encoding specified in the file, use FEQ value
    4.82 +            if (!canEncode(documentContent, feqEncoding)) {
    4.83 +                NotifyDescriptor nd = new NotifyDescriptor.Confirmation(NbBundle.getMessage(TplEditorSupport.class, "MSG_badCharConversionSave", new Object[]{getDataObject().getPrimaryFile().getNameExt(), feqEncoding}), NotifyDescriptor.YES_NO_OPTION, NotifyDescriptor.WARNING_MESSAGE);
    4.84 +                nd.setValue(NotifyDescriptor.NO_OPTION);
    4.85 +                DialogDisplayer.getDefault().notify(nd);
    4.86 +                if (nd.getValue() != NotifyDescriptor.YES_OPTION) {
    4.87 +                    throw new UserCancelException();
    4.88 +                } else {
    4.89 +                    finalEncoding = UTF_8_ENCODING;
    4.90 +                }
    4.91 +            } else {
    4.92 +                finalEncoding = feqEncoding;
    4.93 +            }
    4.94 +        }
    4.95 +
    4.96 +        //FEQ cannot be run in saveFromKitToStream since document is locked for writing,
    4.97 +        //so setting the FEQ result to document property
    4.98 +        getDocument().putProperty(DOCUMENT_SAVE_ENCODING, finalEncoding);
    4.99 +    }
   4.100 +
   4.101 +
   4.102 +    private String getDocumentText() {
   4.103 +        String text = "";
   4.104 +        try {
   4.105 +            StyledDocument doc = getDocument();
   4.106 +            if (doc != null) {
   4.107 +                text = doc.getText(doc.getStartPosition().getOffset(), doc.getLength());
   4.108 +            }
   4.109 +        } catch (BadLocationException e) {
   4.110 +            Logger.getLogger("global").log(Level.WARNING, null, e);
   4.111 +        }
   4.112 +        return text;
   4.113 +    }    
   4.114 +    
   4.115 +
   4.116 +    private boolean canEncode(String docText, String encoding) {
   4.117 +        CharsetEncoder encoder = Charset.forName(encoding).newEncoder();
   4.118 +        return encoder.canEncode(docText);
   4.119 +    }
   4.120 +
   4.121 +    private boolean isSupportedEncoding(String encoding) {
   4.122 +        boolean supported;
   4.123 +        try {
   4.124 +            supported = java.nio.charset.Charset.isSupported(encoding);
   4.125 +        } catch (java.nio.charset.IllegalCharsetNameException e) {
   4.126 +            supported = false;
   4.127 +        }
   4.128 +        return supported;
   4.129 +    }
   4.130 +    
   4.131 +   @Override
   4.132 +    public void open() {
   4.133 +        String encoding = ((TplDataObject) getDataObject()).getFileEncoding();
   4.134 +        String feqEncoding = FileEncodingQuery.getEncoding(getDataObject().getPrimaryFile()).name();
   4.135 +        if (encoding != null && !isSupportedEncoding(encoding)) {
   4.136 +            if(!showConfirmationDialog) {
   4.137 +                return ; //simulate "No" pressed in the dialog if opened
   4.138 +            }
   4.139 +//            if(!canDecodeFile(getDataObject().getPrimaryFile(), feqEncoding)) {
   4.140 +//                feqEncoding = UTF_8_ENCODING;
   4.141 +//            }
   4.142 +            NotifyDescriptor nd = new NotifyDescriptor.Confirmation(
   4.143 +                    NbBundle.getMessage(TplEditorSupport.class, "MSG_unsupportedEncodingLoad", //NOI18N
   4.144 +                    new Object[]{getDataObject().getPrimaryFile().getNameExt(),
   4.145 +                        encoding,
   4.146 +                        feqEncoding}),
   4.147 +                    NotifyDescriptor.YES_NO_OPTION,
   4.148 +                    NotifyDescriptor.WARNING_MESSAGE);
   4.149 +            DialogDisplayer.getDefault().notify(nd);
   4.150 +            if (nd.getValue() != NotifyDescriptor.YES_OPTION) {
   4.151 +                return; // do not open the file
   4.152 +            }
   4.153 +        }
   4.154 +
   4.155 +//        if(!canDecodeFile(getDataObject().getPrimaryFile(), feqEncoding)) {
   4.156 +//            feqEncoding = UTF_8_ENCODING;
   4.157 +//        }
   4.158 +
   4.159 +        super.open();
   4.160 +    }
   4.161 +    
   4.162 +   /**
   4.163       * @inheritDoc
   4.164       */
   4.165      @Override
   4.166      protected void saveFromKitToStream(StyledDocument doc, EditorKit kit, OutputStream stream) throws IOException, BadLocationException {
   4.167 -        final Charset c = Charset.forName("UTF-8");
   4.168 +        final Charset c = Charset.forName((String) doc.getProperty(DOCUMENT_SAVE_ENCODING));
   4.169          final Writer w = new OutputStreamWriter(stream, c);
   4.170          try {
   4.171              kit.write(w, doc, 0, doc.getLength());