Clover icon

Coverage Report

  1. Project Clover database Thu Nov 7 2024 10:11:34 GMT
  2. Package jalview.gui

File AppVarna.java

 

Coverage histogram

../../img/srcFileCovDistChart1.png
56% of files have more coverage

Code metrics

80
206
32
2
783
511
85
0.41
6.44
16
2.66

Classes

Class Line # Actions
AppVarna 63 187 75
0.090592339.1%
AppVarna.VarnaHighlighter 94 19 10
0.00%
 

Contributing tests

This file is covered by 1 test. .

Source view

1    /*
2    * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3    * Copyright (C) $$Year-Rel$$ The Jalview Authors
4    *
5    * This file is part of Jalview.
6    *
7    * Jalview is free software: you can redistribute it and/or
8    * modify it under the terms of the GNU General Public License
9    * as published by the Free Software Foundation, either version 3
10    * of the License, or (at your option) any later version.
11    *
12    * Jalview is distributed in the hope that it will be useful, but
13    * WITHOUT ANY WARRANTY; without even the implied warranty
14    * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
15    * PURPOSE. See the GNU General Public License for more details.
16    *
17    * You should have received a copy of the GNU General Public License
18    * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
19    * The Jalview Authors are detailed in the 'AUTHORS' file.
20    */
21    package jalview.gui;
22   
23    import java.awt.BorderLayout;
24    import java.awt.Color;
25    import java.util.Collection;
26    import java.util.Hashtable;
27    import java.util.LinkedHashMap;
28    import java.util.List;
29    import java.util.Map;
30   
31    import javax.swing.JInternalFrame;
32    import javax.swing.JSplitPane;
33    import javax.swing.event.InternalFrameAdapter;
34    import javax.swing.event.InternalFrameEvent;
35   
36    import fr.orsay.lri.varna.VARNAPanel;
37    import fr.orsay.lri.varna.exceptions.ExceptionFileFormatOrSyntax;
38    import fr.orsay.lri.varna.exceptions.ExceptionLoadingFailed;
39    import fr.orsay.lri.varna.exceptions.ExceptionUnmatchedClosingParentheses;
40    import fr.orsay.lri.varna.interfaces.InterfaceVARNASelectionListener;
41    import fr.orsay.lri.varna.models.BaseList;
42    import fr.orsay.lri.varna.models.FullBackup;
43    import fr.orsay.lri.varna.models.annotations.HighlightRegionAnnotation;
44    import fr.orsay.lri.varna.models.rna.ModeleBase;
45    import fr.orsay.lri.varna.models.rna.RNA;
46    import jalview.analysis.AlignSeq;
47    import jalview.datamodel.AlignmentAnnotation;
48    import jalview.datamodel.ColumnSelection;
49    import jalview.datamodel.HiddenColumns;
50    import jalview.datamodel.RnaViewerModel;
51    import jalview.datamodel.SequenceGroup;
52    import jalview.datamodel.SequenceI;
53    import jalview.ext.varna.RnaModel;
54    import jalview.structure.SecondaryStructureListener;
55    import jalview.structure.SelectionListener;
56    import jalview.structure.SelectionSource;
57    import jalview.structure.StructureSelectionManager;
58    import jalview.structure.VamsasSource;
59    import jalview.util.Comparison;
60    import jalview.util.MessageManager;
61    import jalview.util.ShiftList;
62   
 
63    public class AppVarna extends JInternalFrame
64    implements SelectionListener, SecondaryStructureListener,
65    InterfaceVARNASelectionListener, VamsasSource
66    {
67    private static final byte[] PAIRS = new byte[] { '(', ')', '[', ']', '{',
68    '}', '<', '>' };
69   
70    private AppVarnaBinding vab;
71   
72    private AlignmentPanel ap;
73   
74    private String viewId;
75   
76    private StructureSelectionManager ssm;
77   
78    /*
79    * Lookup for sequence and annotation mapped to each RNA in the viewer. Using
80    * a linked hashmap means that order is preserved when saved to the project.
81    */
82    private Map<RNA, RnaModel> models = new LinkedHashMap<RNA, RnaModel>();
83   
84    private Map<RNA, ShiftList> offsets = new Hashtable<RNA, ShiftList>();
85   
86    private Map<RNA, ShiftList> offsetsInv = new Hashtable<RNA, ShiftList>();
87   
88    private JSplitPane split;
89   
90    private VarnaHighlighter mouseOverHighlighter = new VarnaHighlighter();
91   
92    private VarnaHighlighter selectionHighlighter = new VarnaHighlighter();
93   
 
94    private class VarnaHighlighter
95    {
96    private HighlightRegionAnnotation _lastHighlight;
97   
98    private RNA _lastRNAhighlighted = null;
99   
 
100  0 toggle public VarnaHighlighter()
101    {
102   
103    }
104   
105    /**
106    * Constructor when restoring from Varna session, including any highlight
107    * state
108    *
109    * @param rna
110    */
 
111  0 toggle public VarnaHighlighter(RNA rna)
112    {
113    // TODO nice try but doesn't work; do we need a highlighter per model?
114  0 _lastRNAhighlighted = rna;
115  0 List<HighlightRegionAnnotation> highlights = rna.getHighlightRegion();
116  0 if (highlights != null && !highlights.isEmpty())
117    {
118  0 _lastHighlight = highlights.get(0);
119    }
120    }
121   
122    /**
123    * highlight a region from start to end (inclusive) on rna
124    *
125    * @param rna
126    * @param start
127    * - first base pair index (from 0)
128    * @param end
129    * - last base pair index (from 0)
130    */
 
131  0 toggle public void highlightRegion(RNA rna, int start, int end)
132    {
133  0 clearLastSelection();
134  0 HighlightRegionAnnotation highlight = new HighlightRegionAnnotation(
135    rna.getBasesBetween(start, end));
136  0 rna.addHighlightRegion(highlight);
137  0 _lastHighlight = highlight;
138  0 _lastRNAhighlighted = rna;
139    }
140   
 
141  0 toggle public HighlightRegionAnnotation getLastHighlight()
142    {
143  0 return _lastHighlight;
144    }
145   
146    /**
147    * Clears all structure selection and refreshes the display
148    */
 
149  0 toggle public void clearSelection()
150    {
151  0 if (_lastRNAhighlighted != null)
152    {
153  0 _lastRNAhighlighted.getHighlightRegion().clear();
154  0 vab.updateSelectedRNA(_lastRNAhighlighted);
155  0 _lastRNAhighlighted = null;
156  0 _lastHighlight = null;
157    }
158    }
159   
160    /**
161    * Clear the last structure selection
162    */
 
163  0 toggle public void clearLastSelection()
164    {
165  0 if (_lastRNAhighlighted != null)
166    {
167  0 _lastRNAhighlighted.removeHighlightRegion(_lastHighlight);
168  0 _lastRNAhighlighted = null;
169  0 _lastHighlight = null;
170    }
171    }
172    }
173   
174    /**
175    * Constructor
176    *
177    * @param seq
178    * the RNA sequence
179    * @param aa
180    * the annotation with the secondary structure string
181    * @param ap
182    * the AlignmentPanel creating this object
183    */
 
184  0 toggle public AppVarna(SequenceI seq, AlignmentAnnotation aa, AlignmentPanel ap)
185    {
186  0 this(ap);
187   
188  0 String sname = aa.sequenceRef == null
189    ? "secondary structure (alignment)"
190    : seq.getName() + " structure";
191  0 String theTitle = sname
192  0 + (aa.sequenceRef == null ? " trimmed to " + seq.getName()
193    : "");
194  0 theTitle = MessageManager.formatMessage("label.varna_params",
195    new String[]
196    { theTitle });
197  0 setTitle(theTitle);
198   
199  0 String gappedTitle = sname + " (with gaps)";
200  0 RnaModel gappedModel = new RnaModel(gappedTitle, aa, seq, null, true);
201  0 addModel(gappedModel, gappedTitle);
202   
203  0 String trimmedTitle = "trimmed " + sname;
204  0 RnaModel trimmedModel = new RnaModel(trimmedTitle, aa, seq, null,
205    false);
206  0 addModel(trimmedModel, trimmedTitle);
207  0 vab.setSelectedIndex(0);
208    }
209   
210    /**
211    * Constructor that links the viewer to a parent panel (but has no structures
212    * yet - use addModel to add them)
213    *
214    * @param ap
215    */
 
216  0 toggle protected AppVarna(AlignmentPanel ap)
217    {
218  0 this.setFrameIcon(null);
219  0 this.ap = ap;
220  0 this.viewId = System.currentTimeMillis() + "." + this.hashCode();
221  0 vab = new AppVarnaBinding();
222  0 initVarna();
223   
224  0 this.ssm = ap.getStructureSelectionManager();
225  0 ssm.addStructureViewerListener(this);
226  0 ssm.addSelectionListener(this);
227  0 addInternalFrameListener(new InternalFrameAdapter()
228    {
 
229  0 toggle @Override
230    public void internalFrameClosed(InternalFrameEvent evt)
231    {
232  0 close();
233    }
234    });
235    }
236   
237    /**
238    * Constructor given viewer data read from a saved project file
239    *
240    * @param model
241    * @param ap
242    * the (or a) parent alignment panel
243    */
 
244  0 toggle public AppVarna(RnaViewerModel model, AlignmentPanel ap)
245    {
246  0 this(ap);
247  0 setTitle(model.title);
248  0 this.viewId = model.viewId;
249  0 setBounds(model.x, model.y, model.width, model.height);
250  0 this.split.setDividerLocation(model.dividerLocation);
251    }
252   
253    /**
254    * Constructs a split pane with an empty selection list and display panel, and
255    * adds it to the desktop
256    */
 
257  0 toggle public void initVarna()
258    {
259  0 VARNAPanel varnaPanel = vab.get_varnaPanel();
260  0 setBackground(Color.white);
261  0 split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true,
262    vab.getListPanel(), varnaPanel);
263  0 getContentPane().setLayout(new BorderLayout());
264  0 getContentPane().add(split, BorderLayout.CENTER);
265   
266  0 varnaPanel.addSelectionListener(this);
267  0 jalview.gui.Desktop.addInternalFrame(this, "", getBounds().width,
268    getBounds().height);
269  0 this.pack();
270  0 showPanel(true);
271    }
272   
273    /**
274    * Constructs a new RNA model from the given one, without gaps. Also
275    * calculates and saves a 'shift list'
276    *
277    * @param rna
278    * @param name
279    * @return
280    */
 
281  0 toggle public RNA trimRNA(RNA rna, String name)
282    {
283  0 ShiftList offset = new ShiftList();
284   
285  0 RNA rnaTrim = new RNA(name);
286  0 try
287    {
288  0 String structDBN = rna.getStructDBN(true);
289  0 rnaTrim.setRNA(rna.getSeq(), replaceOddGaps(structDBN));
290    } catch (ExceptionUnmatchedClosingParentheses e2)
291    {
292  0 e2.printStackTrace();
293    } catch (ExceptionFileFormatOrSyntax e3)
294    {
295  0 e3.printStackTrace();
296    }
297   
298  0 String seq = rnaTrim.getSeq();
299  0 StringBuilder struc = new StringBuilder(256);
300  0 struc.append(rnaTrim.getStructDBN(true));
301  0 int ofstart = -1;
302  0 int sleng = seq.length();
303   
304  0 for (int i = 0; i < sleng; i++)
305    {
306  0 if (Comparison.isGap(seq.charAt(i)))
307    {
308  0 if (ofstart == -1)
309    {
310  0 ofstart = i;
311    }
312    /*
313    * mark base or base & pair in the structure with *
314    */
315  0 if (!rnaTrim.findPair(i).isEmpty())
316    {
317  0 int m = rnaTrim.findPair(i).get(1);
318  0 int l = rnaTrim.findPair(i).get(0);
319   
320  0 struc.replace(m, m + 1, "*");
321  0 struc.replace(l, l + 1, "*");
322    }
323    else
324    {
325  0 struc.replace(i, i + 1, "*");
326    }
327    }
328    else
329    {
330  0 if (ofstart > -1)
331    {
332  0 offset.addShift(offset.shift(ofstart), ofstart - i);
333  0 ofstart = -1;
334    }
335    }
336    }
337    // final gap
338  0 if (ofstart > -1)
339    {
340  0 offset.addShift(offset.shift(ofstart), ofstart - sleng);
341  0 ofstart = -1;
342    }
343   
344    /*
345    * remove the marked gaps from the structure
346    */
347  0 String newStruc = struc.toString().replace("*", "");
348   
349    /*
350    * remove gaps from the sequence
351    */
352  0 String newSeq = AlignSeq.extractGaps(Comparison.GapChars, seq);
353   
354  0 try
355    {
356  0 rnaTrim.setRNA(newSeq, newStruc);
357  0 registerOffset(rnaTrim, offset);
358    } catch (ExceptionUnmatchedClosingParentheses e)
359    {
360  0 e.printStackTrace();
361    } catch (ExceptionFileFormatOrSyntax e)
362    {
363  0 e.printStackTrace();
364    }
365  0 return rnaTrim;
366    }
367   
368    /**
369    * Save the sequence to structure mapping, and also its inverse.
370    *
371    * @param rnaTrim
372    * @param offset
373    */
 
374  0 toggle private void registerOffset(RNA rnaTrim, ShiftList offset)
375    {
376  0 offsets.put(rnaTrim, offset);
377  0 offsetsInv.put(rnaTrim, offset.getInverse());
378    }
379   
 
380  0 toggle public void showPanel(boolean show)
381    {
382  0 this.setVisible(show);
383    }
384   
385    /**
386    * If a mouseOver event from the AlignmentPanel is noticed the currently
387    * selected RNA in the VARNA window is highlighted at the specific position.
388    * To be able to remove it before the next highlight it is saved in
389    * _lastHighlight
390    *
391    * @param sequence
392    * @param index
393    * the aligned sequence position (base 0)
394    * @param position
395    * the dataset sequence position (base 1)
396    */
 
397  0 toggle @Override
398    public void mouseOverSequence(SequenceI sequence, final int index,
399    final int position)
400    {
401  0 RNA rna = vab.getSelectedRNA();
402  0 if (rna == null)
403    {
404  0 return;
405    }
406  0 RnaModel rnaModel = models.get(rna);
407  0 if (rnaModel.seq == sequence)
408    {
409  0 int highlightPos = rnaModel.gapped ? index
410    : position - sequence.getStart();
411  0 mouseOverHighlighter.highlightRegion(rna, highlightPos, highlightPos);
412  0 vab.updateSelectedRNA(rna);
413    }
414    }
415   
 
416  0 toggle @Override
417    public void selection(SequenceGroup seqsel, ColumnSelection colsel,
418    HiddenColumns hidden, SelectionSource source)
419    {
420  0 if (source != ap.av)
421    {
422    // ignore events from anything but our parent alignpanel
423    // TODO - reuse many-one panel-view system in jmol viewer
424  0 return;
425    }
426  0 RNA rna = vab.getSelectedRNA();
427  0 if (rna == null)
428    {
429  0 return;
430    }
431   
432  0 RnaModel rnaModel = models.get(rna);
433   
434  0 if (seqsel != null && seqsel.getSize() > 0
435    && seqsel.contains(rnaModel.seq))
436    {
437  0 int start = seqsel.getStartRes(), end = seqsel.getEndRes();
438  0 if (rnaModel.gapped)
439    {
440  0 ShiftList shift = offsets.get(rna);
441  0 if (shift != null)
442    {
443  0 start = shift.shift(start);
444  0 end = shift.shift(end);
445    }
446    }
447    else
448    {
449  0 start = rnaModel.seq.findPosition(start) - rnaModel.seq.getStart();
450  0 end = rnaModel.seq.findPosition(end) - rnaModel.seq.getStart();
451    }
452   
453  0 selectionHighlighter.highlightRegion(rna, start, end);
454  0 selectionHighlighter.getLastHighlight()
455    .setOutlineColor(seqsel.getOutlineColour());
456    // TODO - translate column markings to positions on structure if present.
457  0 vab.updateSelectedRNA(rna);
458    }
459    else
460    {
461  0 selectionHighlighter.clearSelection();
462    }
463    }
464   
465    /**
466    * Respond to a change of the base hovered over in the Varna viewer
467    */
 
468  0 toggle @Override
469    public void onHoverChanged(ModeleBase previousBase, ModeleBase newBase)
470    {
471  0 RNA rna = vab.getSelectedRNA();
472  0 ShiftList shift = offsetsInv.get(rna);
473  0 SequenceI seq = models.get(rna).seq;
474  0 if (newBase != null && seq != null)
475    {
476  0 if (shift != null)
477    {
478  0 int i = shift.shift(newBase.getIndex());
479    // jalview.bin.Console.errPrintln("shifted "+(arg1.getIndex())+" to
480    // "+i);
481  0 ssm.mouseOverVamsasSequence(seq, i, this);
482    }
483    else
484    {
485  0 ssm.mouseOverVamsasSequence(seq, newBase.getIndex(), this);
486    }
487    }
488    }
489   
 
490  0 toggle @Override
491    public void onSelectionChanged(BaseList arg0, BaseList arg1,
492    BaseList arg2)
493    {
494    // TODO translate selected regions in VARNA to a selection on the
495    // alignpanel.
496   
497    }
498   
499    /**
500    * Returns the path to a temporary file containing a representation of the
501    * state of one Varna display
502    *
503    * @param rna
504    *
505    * @return
506    */
 
507  0 toggle public String getStateInfo(RNA rna)
508    {
509  0 return vab.getStateInfo(rna);
510    }
511   
 
512  0 toggle public AlignmentPanel getAlignmentPanel()
513    {
514  0 return ap;
515    }
516   
 
517  0 toggle public String getViewId()
518    {
519  0 return viewId;
520    }
521   
522    /**
523    * Returns true if any of the viewer's models (not necessarily the one
524    * currently displayed) is for the given sequence
525    *
526    * @param seq
527    * @return
528    */
 
529  0 toggle public boolean isListeningFor(SequenceI seq)
530    {
531  0 for (RnaModel model : models.values())
532    {
533  0 if (model.seq == seq)
534    {
535  0 return true;
536    }
537    }
538  0 return false;
539    }
540   
541    /**
542    * Returns a value representing the horizontal split divider location
543    *
544    * @return
545    */
 
546  0 toggle public int getDividerLocation()
547    {
548  0 return split == null ? 0 : split.getDividerLocation();
549    }
550   
551    /**
552    * Tidy up as necessary when the viewer panel is closed
553    */
 
554  0 toggle protected void close()
555    {
556    /*
557    * Deregister as a listener, to release references to this object
558    */
559  0 if (ssm != null)
560    {
561  0 ssm.removeStructureViewerListener(AppVarna.this, null);
562  0 ssm.removeSelectionListener(AppVarna.this);
563    }
564    }
565   
566    /**
567    * Returns the secondary structure annotation that this viewer displays for
568    * the given sequence
569    *
570    * @return
571    */
 
572  0 toggle public AlignmentAnnotation getAnnotation(SequenceI seq)
573    {
574  0 for (RnaModel model : models.values())
575    {
576  0 if (model.seq == seq)
577    {
578  0 return model.ann;
579    }
580    }
581  0 return null;
582    }
583   
 
584  0 toggle public int getSelectedIndex()
585    {
586  0 return this.vab.getSelectedIndex();
587    }
588   
589    /**
590    * Returns the set of models shown by the viewer
591    *
592    * @return
593    */
 
594  0 toggle public Collection<RnaModel> getModels()
595    {
596  0 return models.values();
597    }
598   
599    /**
600    * Add a model (e.g. loaded from project file)
601    *
602    * @param rna
603    * @param modelName
604    */
 
605  0 toggle public RNA addModel(RnaModel model, String modelName)
606    {
607  0 if (!model.ann.isValidStruc())
608    {
609  0 throw new IllegalArgumentException(
610    "Invalid RNA structure annotation");
611    }
612   
613    /*
614    * opened on request in Jalview session
615    */
616  0 RNA rna = new RNA(modelName);
617  0 String struc = model.ann.getRNAStruc();
618  0 struc = replaceOddGaps(struc);
619   
620  0 String strucseq = model.seq.getSequenceAsString();
621  0 try
622    {
623  0 rna.setRNA(strucseq, struc);
624    } catch (ExceptionUnmatchedClosingParentheses e2)
625    {
626  0 e2.printStackTrace();
627    } catch (ExceptionFileFormatOrSyntax e3)
628    {
629  0 e3.printStackTrace();
630    }
631   
632  0 if (!model.gapped)
633    {
634  0 rna = trimRNA(rna, modelName);
635    }
636  0 models.put(rna, new RnaModel(modelName, model.ann, model.seq, rna,
637    model.gapped));
638  0 vab.addStructure(rna);
639  0 return rna;
640    }
641   
642    /**
643    * Constructs a shift list that describes the gaps in the sequence
644    *
645    * @param seq
646    * @return
647    */
 
648  0 toggle protected ShiftList buildOffset(SequenceI seq)
649    {
650    // TODO refactor to avoid duplication with trimRNA()
651    // TODO JAL-1789 bugs in use of ShiftList here
652  0 ShiftList offset = new ShiftList();
653  0 int ofstart = -1;
654  0 int sleng = seq.getLength();
655   
656  0 for (int i = 0; i < sleng; i++)
657    {
658  0 if (Comparison.isGap(seq.getCharAt(i)))
659    {
660  0 if (ofstart == -1)
661    {
662  0 ofstart = i;
663    }
664    }
665    else
666    {
667  0 if (ofstart > -1)
668    {
669  0 offset.addShift(offset.shift(ofstart), ofstart - i);
670  0 ofstart = -1;
671    }
672    }
673    }
674    // final gap
675  0 if (ofstart > -1)
676    {
677  0 offset.addShift(offset.shift(ofstart), ofstart - sleng);
678  0 ofstart = -1;
679    }
680  0 return offset;
681    }
682   
683    /**
684    * Set the selected index in the model selection list
685    *
686    * @param selectedIndex
687    */
 
688  0 toggle public void setInitialSelection(final int selectedIndex)
689    {
690    /*
691    * empirically it needs a second for Varna/AWT to finish loading/drawing
692    * models for this to work; SwingUtilities.invokeLater _not_ a solution;
693    * explanation and/or better solution welcome!
694    */
695  0 synchronized (this)
696    {
697  0 try
698    {
699  0 wait(1000);
700    } catch (InterruptedException e)
701    {
702    // meh
703    }
704    }
705  0 vab.setSelectedIndex(selectedIndex);
706    }
707   
708    /**
709    * Add a model with associated Varna session file
710    *
711    * @param rna
712    * @param modelName
713    */
 
714  0 toggle public RNA addModelSession(RnaModel model, String modelName,
715    String sessionFile)
716    {
717  0 if (!model.ann.isValidStruc())
718    {
719  0 throw new IllegalArgumentException(
720    "Invalid RNA structure annotation");
721    }
722   
723  0 try
724    {
725  0 FullBackup fromSession = vab.vp.loadSession(sessionFile);
726  0 vab.addStructure(fromSession.rna, fromSession.config);
727  0 RNA rna = fromSession.rna;
728    // copy the model, but now including the RNA object
729  0 RnaModel newModel = new RnaModel(model.title, model.ann, model.seq,
730    rna, model.gapped);
731  0 if (!model.gapped)
732    {
733  0 registerOffset(rna, buildOffset(model.seq));
734    }
735  0 models.put(rna, newModel);
736    // capture rna selection state when saved
737  0 selectionHighlighter = new VarnaHighlighter(rna);
738  0 return fromSession.rna;
739    } catch (ExceptionLoadingFailed e)
740    {
741  0 System.err
742    .println("Error restoring Varna session: " + e.getMessage());
743  0 return null;
744    }
745    }
746   
747    /**
748    * Replace everything except RNA secondary structure characters with a period
749    *
750    * @param s
751    * @return
752    */
 
753  5 toggle public static String replaceOddGaps(String s)
754    {
755  5 if (s == null)
756    {
757  1 return null;
758    }
759   
760    // this is measured to be 10 times faster than a regex replace
761  4 boolean changed = false;
762  4 byte[] bytes = s.getBytes();
763  30 for (int i = 0; i < bytes.length; i++)
764    {
765  26 boolean ok = false;
766    // todo check for ((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')) if
767    // wanted also
768  183 for (int j = 0; !ok && (j < PAIRS.length); j++)
769    {
770  157 if (bytes[i] == PAIRS[j])
771    {
772  15 ok = true;
773    }
774    }
775  26 if (!ok)
776    {
777  11 bytes[i] = '.';
778  11 changed = true;
779    }
780    }
781  4 return changed ? new String(bytes) : s;
782    }
783    }