X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fjalview%2Fgui%2FAppVarna.java;h=37428642a15ab52a9283fef6fd9c2317032b88d0;hb=17e77c3f2949a0729322b4a8d907f3f34b6a9914;hp=da6c6e5325b9b48941cc31c903407a5a3e249f64;hpb=3e087813771dee914b78a955bf66ca4803e89797;p=jalview.git diff --git a/src/jalview/gui/AppVarna.java b/src/jalview/gui/AppVarna.java index da6c6e5..3742864 100644 --- a/src/jalview/gui/AppVarna.java +++ b/src/jalview/gui/AppVarna.java @@ -1,157 +1,278 @@ /* - * Jalview - A Sequence Alignment Editor and Viewer (Version 2.7) - * Copyright (C) 2011 J Procter, AM Waterhouse, J Engelhardt, LM Lui, G Barton, M Clamp, S Searle + * Jalview - A Sequence Alignment Editor and Viewer (Version 2.9) + * Copyright (C) 2015 The Jalview Authors * * This file is part of Jalview. * * Jalview is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - * + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * * Jalview is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty * of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * - * You should have received a copy of the GNU General Public License along with Jalview. If not, see . + * You should have received a copy of the GNU General Public License + * along with Jalview. If not, see . + * The Jalview Authors are detailed in the 'AUTHORS' file. */ package jalview.gui; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.awt.*; - -import javax.swing.*; -import javax.swing.event.*; - -import java.awt.event.*; -import java.io.*; - -import jalview.api.AlignViewportI; -import jalview.api.AlignmentViewPanel; -import jalview.api.SequenceStructureBinding; -import jalview.bin.Cache; -import jalview.datamodel.*; -import jalview.gui.ViewSelectionMenu.ViewSetProvider; -import jalview.structure.*; -import jalview.io.*; -import jalview.schemes.*; +import jalview.analysis.AlignSeq; +import jalview.datamodel.AlignmentAnnotation; +import jalview.datamodel.ColumnSelection; +import jalview.datamodel.RnaViewerModel; +import jalview.datamodel.SequenceGroup; +import jalview.datamodel.SequenceI; +import jalview.ext.varna.RnaModel; +import jalview.structure.SecondaryStructureListener; +import jalview.structure.SelectionListener; +import jalview.structure.SelectionSource; +import jalview.structure.StructureSelectionManager; +import jalview.structure.VamsasSource; +import jalview.util.Comparison; +import jalview.util.MessageManager; import jalview.util.ShiftList; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.util.Collection; +import java.util.Hashtable; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.swing.JInternalFrame; +import javax.swing.JSplitPane; +import javax.swing.event.InternalFrameAdapter; +import javax.swing.event.InternalFrameEvent; + import fr.orsay.lri.varna.VARNAPanel; import fr.orsay.lri.varna.exceptions.ExceptionFileFormatOrSyntax; -import fr.orsay.lri.varna.exceptions.ExceptionNonEqualLength; +import fr.orsay.lri.varna.exceptions.ExceptionLoadingFailed; import fr.orsay.lri.varna.exceptions.ExceptionUnmatchedClosingParentheses; -import fr.orsay.lri.varna.interfaces.InterfaceVARNAListener; import fr.orsay.lri.varna.interfaces.InterfaceVARNASelectionListener; import fr.orsay.lri.varna.models.BaseList; -import fr.orsay.lri.varna.models.VARNAConfig; +import fr.orsay.lri.varna.models.FullBackup; import fr.orsay.lri.varna.models.annotations.HighlightRegionAnnotation; import fr.orsay.lri.varna.models.rna.ModeleBase; -import fr.orsay.lri.varna.models.rna.ModeleBaseNucleotide; import fr.orsay.lri.varna.models.rna.RNA; -public class AppVarna extends JInternalFrame implements - InterfaceVARNAListener, SelectionListener, - SecondaryStructureListener// implements - // Runnable,SequenceStructureBinding, - // ViewSetProvider - , InterfaceVARNASelectionListener, VamsasSource - +public class AppVarna extends JInternalFrame implements SelectionListener, + SecondaryStructureListener, InterfaceVARNASelectionListener, + VamsasSource { - AppVarnaBinding vab; + private static final byte[] PAIRS = new byte[] { '(', ')', '[', ']', '{', + '}', '<', '>' }; - VARNAPanel varnaPanel; + private AppVarnaBinding vab; - public String name; + private AlignmentPanel ap; - public StructureSelectionManager ssm; + private String viewId; + + private StructureSelectionManager ssm; /* - * public AppVarna(){ vab = new AppVarnaBinding(); initVarna(); } + * Lookup for sequence and annotation mapped to each RNA in the viewer. Using + * a linked hashmap means that order is preserved when saved to the project. */ + private Map models = new LinkedHashMap(); + + private Map offsets = new Hashtable(); + + private Map offsetsInv = new Hashtable(); + + private JSplitPane split; + + private VarnaHighlighter mouseOverHighlighter = new VarnaHighlighter(); - AlignmentPanel ap; + private VarnaHighlighter selectionHighlighter = new VarnaHighlighter(); - public AppVarna(String sname, SequenceI seq, String strucseq, String struc, - String name, AlignmentPanel ap) + private class VarnaHighlighter { - this.ap = ap; - ArrayList rnaList = new ArrayList(); - RNA rna1 = new RNA(name); - try + private HighlightRegionAnnotation _lastHighlight; + + private RNA _lastRNAhighlighted = null; + + public VarnaHighlighter() { - rna1.setRNA(strucseq, replaceOddGaps(struc)); - } catch (ExceptionUnmatchedClosingParentheses e2) + + } + + /** + * Constructor when restoring from Varna session, including any highlight + * state + * + * @param rna + */ + public VarnaHighlighter(RNA rna) { - e2.printStackTrace(); - } catch (ExceptionFileFormatOrSyntax e3) + // TODO nice try but doesn't work; do we need a highlighter per model? + _lastRNAhighlighted = rna; + List highlights = rna.getHighlightRegion(); + if (highlights != null && !highlights.isEmpty()) + { + _lastHighlight = highlights.get(0); + } + } + + public void highlightRegion(RNA rna, int start, int end) { - e3.printStackTrace(); + clearLastSelection(); + HighlightRegionAnnotation highlight = new HighlightRegionAnnotation( + rna.getBasesBetween(start, end)); + rna.addHighlightRegion(highlight); + _lastHighlight = highlight; + _lastRNAhighlighted = rna; + } + + public HighlightRegionAnnotation getLastHighlight() + { + return _lastHighlight; + } + + /** + * Clears all structure selection and refreshes the display + */ + public void clearSelection() + { + if (_lastRNAhighlighted != null) + { + _lastRNAhighlighted.getHighlightRegion().clear(); + vab.updateSelectedRNA(_lastRNAhighlighted); + _lastRNAhighlighted = null; + _lastHighlight = null; + } } - RNA trim = trimRNA(rna1, "trimmed "+sname); - rnaList.add(trim); - rnaList.add(rna1); - rnas.put(seq, rna1); - rnas.put(seq, trim); - rna1.setName(sname+" (with gaps)"); - - { - seqs.put(trim, seq); - seqs.put(rna1, seq); - - /** - * if (false || seq.getStart()!=1) { for (RNA rshift:rnaList) { ShiftList - * shift=offsets.get(rshift); if (shift==null) { offsets.put(rshift, - * shift=new ShiftList());} shift.addShift(1, seq.getStart()-1); - * offsetsInv.put(rshift, shift.getInverse()); } } - **/ + + /** + * Clear the last structure selection + */ + public void clearLastSelection() + { + if (_lastRNAhighlighted != null) + { + _lastRNAhighlighted.removeHighlightRegion(_lastHighlight); + _lastRNAhighlighted = null; + _lastHighlight = null; + } } - vab = new AppVarnaBinding(rnaList); - // vab = new AppVarnaBinding(seq,struc); - // System.out.println("Hallo: "+name); - this.name = sname+" trimmed to "+name; + } + + /** + * Constructor + * + * @param seq + * the RNA sequence + * @param aa + * the annotation with the secondary structure string + * @param ap + * the AlignmentPanel creating this object + */ + public AppVarna(SequenceI seq, AlignmentAnnotation aa, AlignmentPanel ap) + { + this(ap); + + String sname = aa.sequenceRef == null ? "secondary structure (alignment)" + : seq.getName() + " structure"; + String theTitle = sname + + (aa.sequenceRef == null ? " trimmed to " + seq.getName() : ""); + theTitle = MessageManager.formatMessage("label.varna_params", + new String[] { theTitle }); + setTitle(theTitle); + + String gappedTitle = sname + " (with gaps)"; + RnaModel gappedModel = new RnaModel(gappedTitle, aa, seq, null, true); + addModel(gappedModel, gappedTitle); + + String trimmedTitle = "trimmed " + sname; + RnaModel trimmedModel = new RnaModel(trimmedTitle, aa, seq, null, false); + addModel(trimmedModel, trimmedTitle); + vab.setSelectedIndex(0); + } + + /** + * Constructor that links the viewer to a parent panel (but has no structures + * yet - use addModel to add them) + * + * @param ap + */ + protected AppVarna(AlignmentPanel ap) + { + this.ap = ap; + this.viewId = System.currentTimeMillis() + "." + this.hashCode(); + vab = new AppVarnaBinding(); initVarna(); - ssm = ap.getStructureSelectionManager(); + + this.ssm = ap.getStructureSelectionManager(); ssm.addStructureViewerListener(this); ssm.addSelectionListener(this); + addInternalFrameListener(new InternalFrameAdapter() + { + @Override + public void internalFrameClosed(InternalFrameEvent evt) + { + close(); + } + }); } + /** + * Constructor given viewer data read from a saved project file + * + * @param model + * @param ap + * the (or a) parent alignment panel + */ + public AppVarna(RnaViewerModel model, AlignmentPanel ap) + { + this(ap); + setTitle(model.title); + this.viewId = model.viewId; + setBounds(model.x, model.y, model.width, model.height); + this.split.setDividerLocation(model.dividerLocation); + } + + /** + * Constructs a split pane with an empty selection list and display panel, and + * adds it to the desktop + */ public void initVarna() { - // vab.setFinishedInit(false); - varnaPanel = vab.get_varnaPanel(); + VARNAPanel varnaPanel = vab.get_varnaPanel(); setBackground(Color.white); - JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, + split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, vab.getListPanel(), varnaPanel); getContentPane().setLayout(new BorderLayout()); getContentPane().add(split, BorderLayout.CENTER); - // getContentPane().add(vab.getTools(), BorderLayout.NORTH); - varnaPanel.addVARNAListener(this); + varnaPanel.addSelectionListener(this); - jalview.gui.Desktop.addInternalFrame(this, "VARNA -" + name, - getBounds().width, getBounds().height); + jalview.gui.Desktop.addInternalFrame(this, "", getBounds().width, + getBounds().height); this.pack(); showPanel(true); } - public String replaceOddGaps(String oldStr) - { - String patternStr = "[^([{<>}])]"; - String replacementStr = "."; - Pattern pattern = Pattern.compile(patternStr); - Matcher matcher = pattern.matcher(oldStr); - String newStr = matcher.replaceAll(replacementStr); - return newStr; - } - + /** + * Constructs a new RNA model from the given one, without gaps. Also + * calculates and saves a 'shift list' + * + * @param rna + * @param name + * @return + */ public RNA trimRNA(RNA rna, String name) { ShiftList offset = new ShiftList(); + RNA rnaTrim = new RNA(name); try { - rnaTrim.setRNA(rna.getSeq(), replaceOddGaps(rna.getStructDBN())); + String structDBN = rna.getStructDBN(true); + rnaTrim.setRNA(rna.getSeq(), replaceOddGaps(structDBN)); } catch (ExceptionUnmatchedClosingParentheses e2) { e2.printStackTrace(); @@ -160,19 +281,23 @@ public class AppVarna extends JInternalFrame implements e3.printStackTrace(); } - StringBuffer seq = new StringBuffer(rnaTrim.getSeq()); - StringBuffer struc = new StringBuffer(rnaTrim.getStructDBN()); - int ofstart = -1, sleng = rnaTrim.getSeq().length(); + String seq = rnaTrim.getSeq(); + StringBuilder struc = new StringBuilder(256); + struc.append(rnaTrim.getStructDBN(true)); + int ofstart = -1; + int sleng = seq.length(); + for (int i = 0; i < sleng; i++) { - // TODO: Jalview utility for gap detection java.utils.isGap() - // TODO: Switch to jalview rna datamodel - if (jalview.util.Comparison.isGap(seq.charAt(i))) + if (Comparison.isGap(seq.charAt(i))) { if (ofstart == -1) { ofstart = i; } + /* + * mark base or base & pair in the structure with * + */ if (!rnaTrim.findPair(i).isEmpty()) { int m = rnaTrim.findPair(i).get(1); @@ -201,35 +326,37 @@ public class AppVarna extends JInternalFrame implements offset.addShift(offset.shift(ofstart), ofstart - sleng); ofstart = -1; } - String newSeq = rnaTrim.getSeq().replace("-", ""); - rnaTrim.getSeq().replace(".", ""); + + /* + * remove the marked gaps from the structure + */ String newStruc = struc.toString().replace("*", ""); + /* + * remove gaps from the sequence + */ + String newSeq = AlignSeq.extractGaps(Comparison.GapChars, seq); + try { rnaTrim.setRNA(newSeq, newStruc); registerOffset(rnaTrim, offset); } catch (ExceptionUnmatchedClosingParentheses e) { - // TODO Auto-generated catch block e.printStackTrace(); } catch (ExceptionFileFormatOrSyntax e) { - // TODO Auto-generated catch block e.printStackTrace(); } return rnaTrim; } - // needs to be many-many - Map seqs = new Hashtable(); - - Map rnas = new Hashtable(); - - Map offsets = new Hashtable(); - - Map offsetsInv = new Hashtable(); - + /** + * Save the sequence to structure mapping, and also its inverse. + * + * @param rnaTrim + * @param offset + */ private void registerOffset(RNA rnaTrim, ShiftList offset) { offsets.put(rnaTrim, offset); @@ -241,115 +368,37 @@ public class AppVarna extends JInternalFrame implements this.setVisible(show); } - private boolean _started = false; - - public void run() - { - _started = true; - - try - { - initVarna(); - } catch (OutOfMemoryError oomerror) - { - new OOMWarning("When trying to open the Varna viewer!", oomerror); - } catch (Exception ex) - { - Cache.log.error("Couldn't open Varna viewer!", ex); - } - } - - @Override - public void onUINewStructure(VARNAConfig v, RNA r) - { - - } - - @Override - public void onWarningEmitted(String s) - { - // TODO Auto-generated method stub - - } - - private class VarnaHighlighter - { - private HighlightRegionAnnotation _lastHighlight; - - private RNA _lastRNAhighlighted = null; - - public void highlightRegion(RNA rna, int start, int end) - { - clearSelection(null); - HighlightRegionAnnotation highlight = new HighlightRegionAnnotation( - rna.getBasesBetween(start, end)); - rna.addHighlightRegion(highlight); - _lastHighlight = highlight; - _lastRNAhighlighted = rna; - - } - - public HighlightRegionAnnotation getLastHighlight() - { - return _lastHighlight; - } - - public RNA getLastRNA() - { - return _lastRNAhighlighted; - } - - public void clearSelection(AppVarnaBinding vab) - { - if (_lastRNAhighlighted != null) - { - _lastRNAhighlighted.removeHighlightRegion(_lastHighlight); - if (vab != null) - { - vab.updateSelectedRNA(_lastRNAhighlighted); - } - _lastRNAhighlighted = null; - _lastHighlight = null; - - } - } - } - - VarnaHighlighter mouseOverHighlighter = new VarnaHighlighter(), - selectionHighlighter = new VarnaHighlighter(); - /** * If a mouseOver event from the AlignmentPanel is noticed the currently * selected RNA in the VARNA window is highlighted at the specific position. * To be able to remove it before the next highlight it is saved in * _lastHighlight + * + * @param sequence + * @param index + * the aligned sequence position (base 0) + * @param position + * the dataset sequence position (base 1) */ @Override - public void mouseOverSequence(SequenceI sequence, int index) + public void mouseOverSequence(SequenceI sequence, final int index, + final int position) { RNA rna = vab.getSelectedRNA(); - if (seqs.get(rna) == sequence) + if (rna == null) { - ShiftList shift = offsets.get(rna); - if (shift != null) - { - // System.err.print("Orig pos:"+index); - index = shift.shift(index); - // System.err.println("\nFinal pos:"+index); - } - mouseOverHighlighter.highlightRegion(rna, index, index); + return; + } + RnaModel rnaModel = models.get(rna); + if (rnaModel.seq == sequence) + { + int highlightPos = rnaModel.gapped ? index : position - 1; + mouseOverHighlighter.highlightRegion(rna, highlightPos, highlightPos); vab.updateSelectedRNA(rna); } } @Override - public void onStructureRedrawn() - { - // TODO Auto-generated method stub - - } - - @Override public void selection(SequenceGroup seqsel, ColumnSelection colsel, SelectionSource source) { @@ -359,10 +408,14 @@ public class AppVarna extends JInternalFrame implements // TODO - reuse many-one panel-view system in jmol viewer return; } + RNA rna = vab.getSelectedRNA(); + if (rna == null) + { + return; + } if (seqsel != null && seqsel.getSize() > 0) { int start = seqsel.getStartRes(), end = seqsel.getEndRes(); - RNA rna = vab.getSelectedRNA(); ShiftList shift = offsets.get(rna); if (shift != null) { @@ -377,27 +430,30 @@ public class AppVarna extends JInternalFrame implements } else { - selectionHighlighter.clearSelection(vab); + selectionHighlighter.clearSelection(); } } + /** + * Respond to a change of the base hovered over in the Varna viewer + */ @Override - public void onHoverChanged(ModeleBase arg0, ModeleBase arg1) + public void onHoverChanged(ModeleBase previousBase, ModeleBase newBase) { RNA rna = vab.getSelectedRNA(); ShiftList shift = offsetsInv.get(rna); - SequenceI seq = seqs.get(rna); - if (arg1 != null && seq != null) + SequenceI seq = models.get(rna).seq; + if (newBase != null && seq != null) { if (shift != null) { - int i = shift.shift(arg1.getIndex()); - // System.err.println("shifted "+(arg1.getIndex())+" to "+i); + int i = shift.shift(newBase.getIndex()); + // System.err.println("shifted "+(arg1.getIndex())+" to "+i); ssm.mouseOverVamsasSequence(seq, i, this); } else { - ssm.mouseOverVamsasSequence(seq, arg1.getIndex(), this); + ssm.mouseOverVamsasSequence(seq, newBase.getIndex(), this); } } } @@ -410,4 +466,287 @@ public class AppVarna extends JInternalFrame implements } + /** + * Returns the path to a temporary file containing a representation of the + * state of one Varna display + * + * @param rna + * + * @return + */ + public String getStateInfo(RNA rna) + { + return vab.getStateInfo(rna); + } + + public AlignmentPanel getAlignmentPanel() + { + return ap; + } + + public String getViewId() + { + return viewId; + } + + /** + * Returns true if any of the viewer's models (not necessarily the one + * currently displayed) is for the given sequence + * + * @param seq + * @return + */ + public boolean isListeningFor(SequenceI seq) + { + for (RnaModel model : models.values()) + { + if (model.seq == seq) + { + return true; + } + } + return false; + } + + /** + * Returns a value representing the horizontal split divider location + * + * @return + */ + public int getDividerLocation() + { + return split == null ? 0 : split.getDividerLocation(); + } + + /** + * Tidy up as necessary when the viewer panel is closed + */ + protected void close() + { + /* + * Deregister as a listener, to release references to this object + */ + if (ssm != null) + { + ssm.removeStructureViewerListener(AppVarna.this, null); + ssm.removeSelectionListener(AppVarna.this); + } + } + + /** + * Returns the secondary structure annotation that this viewer displays for + * the given sequence + * + * @return + */ + public AlignmentAnnotation getAnnotation(SequenceI seq) + { + for (RnaModel model : models.values()) + { + if (model.seq == seq) + { + return model.ann; + } + } + return null; + } + + public int getSelectedIndex() + { + return this.vab.getSelectedIndex(); + } + + /** + * Returns the set of models shown by the viewer + * + * @return + */ + public Collection getModels() + { + return models.values(); + } + + /** + * Add a model (e.g. loaded from project file) + * + * @param rna + * @param modelName + */ + public RNA addModel(RnaModel model, String modelName) + { + if (!model.ann.isValidStruc()) + { + throw new IllegalArgumentException("Invalid RNA structure annotation"); + } + + /* + * opened on request in Jalview session + */ + RNA rna = new RNA(modelName); + String struc = model.ann.getRNAStruc(); + struc = replaceOddGaps(struc); + + String strucseq = model.seq.getSequenceAsString(); + try + { + rna.setRNA(strucseq, struc); + } catch (ExceptionUnmatchedClosingParentheses e2) + { + e2.printStackTrace(); + } catch (ExceptionFileFormatOrSyntax e3) + { + e3.printStackTrace(); + } + + if (!model.gapped) + { + rna = trimRNA(rna, modelName); + } + models.put(rna, new RnaModel(modelName, model.ann, model.seq, rna, + model.gapped)); + vab.addStructure(rna); + return rna; + } + + /** + * Constructs a shift list that describes the gaps in the sequence + * + * @param seq + * @return + */ + protected ShiftList buildOffset(SequenceI seq) + { + // TODO refactor to avoid duplication with trimRNA() + // TODO JAL-1789 bugs in use of ShiftList here + ShiftList offset = new ShiftList(); + int ofstart = -1; + int sleng = seq.getLength(); + char[] seqChars = seq.getSequence(); + + for (int i = 0; i < sleng; i++) + { + if (Comparison.isGap(seqChars[i])) + { + if (ofstart == -1) + { + ofstart = i; + } + } + else + { + if (ofstart > -1) + { + offset.addShift(offset.shift(ofstart), ofstart - i); + ofstart = -1; + } + } + } + // final gap + if (ofstart > -1) + { + offset.addShift(offset.shift(ofstart), ofstart - sleng); + ofstart = -1; + } + return offset; + } + + /** + * Set the selected index in the model selection list + * + * @param selectedIndex + */ + public void setInitialSelection(final int selectedIndex) + { + /* + * empirically it needs a second for Varna/AWT to finish loading/drawing + * models for this to work; SwingUtilities.invokeLater _not_ a solution; + * explanation and/or better solution welcome! + */ + synchronized (this) + { + try + { + wait(1000); + } catch (InterruptedException e) + { + // meh + } + } + vab.setSelectedIndex(selectedIndex); + } + + /** + * Add a model with associated Varna session file + * + * @param rna + * @param modelName + */ + public RNA addModelSession(RnaModel model, String modelName, + String sessionFile) + { + if (!model.ann.isValidStruc()) + { + throw new IllegalArgumentException("Invalid RNA structure annotation"); + } + + try + { + FullBackup fromSession = vab.vp.loadSession(sessionFile); + vab.addStructure(fromSession.rna, fromSession.config); + RNA rna = fromSession.rna; + // copy the model, but now including the RNA object + RnaModel newModel = new RnaModel(model.title, model.ann, model.seq, + rna, model.gapped); + if (!model.gapped) + { + registerOffset(rna, buildOffset(model.seq)); + } + models.put(rna, newModel); + // capture rna selection state when saved + selectionHighlighter = new VarnaHighlighter(rna); + return fromSession.rna; + } catch (ExceptionLoadingFailed e) + { + System.err + .println("Error restoring Varna session: " + e.getMessage()); + return null; + } + } + + /** + * Replace everything except RNA secondary structure characters with a period + * + * @param s + * @return + */ + public static String replaceOddGaps(String s) + { + if (s == null) + { + return null; + } + + // this is measured to be 10 times faster than a regex replace + boolean changed = false; + byte[] bytes = s.getBytes(); + for (int i = 0; i < bytes.length; i++) + { + boolean ok = false; + // todo check for ((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')) if + // wanted also + for (int j = 0; !ok && (j < PAIRS.length); j++) + { + if (bytes[i] == PAIRS[j]) + { + ok = true; + } + } + if (!ok) + { + bytes[i] = '.'; + changed = true; + } + } + return changed ? new String(bytes) : s; + } }