+package javajs.util;
+
+import java.io.BufferedReader;
+
+import java.util.Hashtable;
+
+import java.util.Map;
+
+import javajs.api.GenericCifDataParser;
+import javajs.api.GenericLineReader;
+
+
+// BH 11/21/16 -- adds support for array grouping [...] - used in 2016-format magCIF files
+
+/**
+*
+* A CIF 1.0 tokenizer class for dealing with quoted strings in CIF files.
+*
+* Subclassed by org.jmol.adapters.readers.cif.Cif2DataParser
+*
+* Greek letters implemented in Jmol 13.3.9 and only for
+* titles and space groups. All other mark ups ignored.
+*
+*<p>
+* regarding the treatment of single quotes vs. primes in
+* cif file, PMR wrote:
+*</p>
+*<p>
+* * There is a formal grammar for CIF
+* (see http://www.iucr.org/iucr-top/cif/index.html)
+* which confirms this. The textual explanation is
+*<p />
+*<p>
+* 14. Matching single or double quote characters (' or ") may
+* be used to bound a string representing a non-simple data value
+* provided the string does not extend over more than one line.
+*<p />
+*<p>
+* 15. Because data values are invariably separated from other
+* tokens in the file by white space, such a quote-delimited
+* character string may contain instances of the character used
+* to delimit the string provided they are not followed by white
+* space. For example, the data item
+*<code>
+* _example 'a dog's life'
+*</code>
+* is legal; the data value is a dog's life.
+*</p>
+*<p>
+* [PMR - the terminating character(s) are quote+whitespace.
+* That would mean that:
+*<code>
+* _example 'Jones' life'
+*</code>
+* would be an error
+*</p>
+*<p>
+* The CIF format was developed in that late 1980's under the aegis of the
+* International Union of Crystallography (I am a consultant to the COMCIFs
+* committee). It was ratified by the Union and there have been several
+* workshops. mmCIF is an extension of CIF which includes a relational
+* structure. The formal publications are:
+*</p>
+*<p>
+* Hall, S. R. (1991). "The STAR File: A New Format for Electronic Data
+* Transfer and Archiving", J. Chem. Inform. Comp. Sci., 31, 326-333.
+* Hall, S. R., Allen, F. H. and Brown, I. D. (1991). "The Crystallographic
+* Information File (CIF): A New Standard Archive File for Crystallography",
+* Acta Cryst., A47, 655-685.
+* Hall, S.R. & Spadaccini, N. (1994). "The STAR File: Detailed
+* Specifications," J. Chem. Info. Comp. Sci., 34, 505-508.
+*</p>
+*/
+
+public class CifDataParser implements GenericCifDataParser {
+
+ protected int getVersion() {
+ return 1;
+ }
+
+ /**
+ * The maximum number of columns (data keys) passed to the parser or found in the file
+ * for a given loop_ or category.subkey listing.
+ *
+ */
+ public static final int KEY_MAX = 100;
+
+ private GenericLineReader reader;
+ private BufferedReader br;
+
+ /**
+ * from buffered reader
+ */
+ protected String line;
+
+ /**
+ * working string (buffer)
+ *
+ */
+ protected String str;
+
+ /**
+ * pointer to current character on str
+ */
+ protected int ich;
+
+ /**
+ * length of str
+ *
+ */
+ protected int cch;
+
+ /**
+ * whether we are processing an unquoted value or key
+ */
+ protected boolean wasUnquoted;
+
+ /**
+ * optional token terminator; in CIF 2.0 could be } or ]
+ */
+ protected char cterm = '\0';
+
+ /**
+ * string to return for CIF data value . and ?
+ */
+ protected String nullString = "\0";
+
+ /**
+ * A flag to create and return Java objects, not strings.
+ * Used only by Jmol scripting x = getProperty("cifInfo", filename).
+ */
+ protected boolean asObject;
+
+
+ /**
+ * debugging flag passed from reader; unused
+ *
+ */
+ protected boolean debugging;
+
+
+ /**
+ * private processing fields
+ *
+ */
+ private Object strPeeked;
+ private int ichPeeked;
+ private int columnCount;
+ private String[] columnNames;
+ private Object[] columnData = new Object[KEY_MAX];
+ private boolean isLoop;
+ private boolean haveData;
+
+ /**
+ * comments at the top of a file, including #\#CIF_2.0, for example
+ */
+ private SB fileHeader = new SB();
+ private boolean isHeader = true;
+
+
+ /**
+ * Set the string value of what is returned for "." and "?"
+ *
+ * @param nullString null here returns "." and "?"; default is "\0"
+ *
+ */
+ public void setNullValue(String nullString) {
+ this.nullString = nullString;
+ }
+
+ /**
+ * A global, static map that contains field information. The assumption is that
+ * if we read a set of fields for, say, atom_site, once in a lifetime, then
+ * that should be good forever. Those are static lists. Or should be....
+ */
+ private static Map<String, Integer> htFields = new Hashtable<String, Integer>();
+
+ ////////////////////////////////////////////////////////////////
+ // special tokenizer class
+ ////////////////////////////////////////////////////////////////
+
+ public CifDataParser() {
+ // for reflection
+ }
+
+ @Override
+ public Object getColumnData(int i) {
+ return columnData[i];
+ }
+
+ @Override
+ public int getColumnCount() {
+ return columnCount;
+ }
+
+ @Override
+ public String getColumnName(int i) {
+ return columnNames[i];
+ }
+
+ /**
+ * A Chemical Information File data parser.
+ *
+ * set() should be called immediately upon construction.
+ *
+ * Two options; one of reader or br should be null, or reader will be
+ * ignored. Just simpler this way...
+ *
+ * @param reader Anything that can deliver a line of text or null
+ * @param br A standard BufferedReader.
+ * @param debugging
+ *
+ */
+ @Override
+ public CifDataParser set(GenericLineReader reader, BufferedReader br, boolean debugging) {
+ this.reader = reader;
+ this.br = br;
+ this.debugging = debugging;
+ return this;
+ }
+
+
+ /**
+ *
+ * @return commented-out section at the start of a CIF file.
+ *
+ */
+ @Override
+ public String getFileHeader() {
+ return fileHeader.toString();
+ }
+
+
+ /**
+ * Parses all CIF data for a reader defined in the constructor
+ * into a standard Map structure and close the BufferedReader if
+ * it exists.
+ *
+ * @return Hashtable of models Vector of Hashtable data
+ */
+ @Override
+ public Map<String, Object> getAllCifData() {
+ line = "";
+ String key;
+ Map<String, Object> data = null, data0 = null;
+ Map<String, Object> allData = new Hashtable<String, Object>();
+ Lst<Map<String, Object>> models = new Lst<Map<String,Object>>();
+ allData.put("models", models);
+ asObject = (getVersion() >= 2);
+ nullString = null;
+ Lst<Map<String, Object>> saveFrames = new Lst<Map<String, Object>>();
+ try {
+ while ((key = getNextToken()) != null) {
+ if (key.startsWith("global_") || key.startsWith("data_")) {
+ models.addLast(data0 = data = new Hashtable<String, Object>());
+ data.put("name", key);
+ continue;
+ }
+ if (key.startsWith("loop_")) {
+ getAllCifLoopData(data);
+ continue;
+ }
+ if (key.startsWith("save_")) {
+ if (key.equals("save_")) {
+ int n = saveFrames.size();
+ if (n == 0) {
+ System.out.println("CIF ERROR ? save_ without corresponding save_xxxx");
+ data = data0;
+ } else {
+ data = saveFrames.removeItemAt(n - 1);
+ }
+ } else {
+ saveFrames.addLast(data);
+ Map<String, Object> d = data;
+ data = new Hashtable<String, Object>();
+ d.put(key, data);
+ }
+ continue;
+ }
+ if (key.charAt(0) != '_') {
+ System.out.println("CIF ERROR ? should be an underscore: " + key);
+ } else {
+ Object value = (asObject ? getNextTokenObject() : getNextToken());
+ if (value == null) {
+ System.out.println("CIF ERROR ? end of file; data missing: " + key);
+ } else {
+ data.put(fixKey(key), value);
+ }
+ }
+ }
+ } catch (Exception e) {
+ // ?
+ }
+ asObject = false;
+ try {
+ if (br != null)
+ br.close();
+ } catch (Exception e) {
+ // ?
+ }
+ nullString = "\0";
+ return allData;
+ }
+
+ /**
+ * create our own list of keywords and for each one create a list
+ * of data associated with that keyword. For example, a list of all
+ * x coordinates, then a list of all y coordinates, etc.
+ *
+ * @param data
+ * @throws Exception
+ */
+ @SuppressWarnings("unchecked")
+ private void getAllCifLoopData(Map<String, Object> data) throws Exception {
+ String key;
+ Lst<String> keyWords = new Lst<String>();
+ Object o;
+ while ((o = peekToken()) != null && o instanceof String && ((String) o).charAt(0) == '_') {
+ key = fixKey((String) getTokenPeeked());
+ keyWords.addLast(key);
+ data.put(key, new Lst<String>());
+ }
+ columnCount = keyWords.size();
+ if (columnCount == 0)
+ return;
+ isLoop = true;
+ while (getData())
+ for (int i = 0; i < columnCount; i++)
+ ((Lst<Object>)data.get(keyWords.get(i))).addLast(columnData[i]);
+ isLoop = false;
+ }
+
+ @Override
+ public String readLine() {
+ try {
+ line = (reader == null ? br.readLine() : reader.readNextLine());
+ if (line == null)
+ return null;
+ if (isHeader) {
+ if (line.startsWith("#"))
+ fileHeader.append(line).appendC('\n');
+ else
+ isHeader = false;
+ }
+ return line;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * The work horse; a general reader for loop data. Fills colunnData with
+ * fieldCount fields.
+ *
+ * @return false if EOF
+ * @throws Exception
+ */
+ @Override
+ public boolean getData() throws Exception {
+ // line is already present, and we leave with the next line to parse
+ if (isLoop) {
+ for (int i = 0; i < columnCount; ++i)
+ if ((columnData[i] = getNextDataToken()) == null)
+ return false;
+ } else if (haveData) {
+ haveData = false;
+ } else {
+ return false;
+ }
+ return (columnCount > 0);
+ }
+
+ /**
+ *
+ * Skips all associated loop data. (Skips to next control word.)
+ *
+ * @throws Exception
+ */
+ @Override
+ public String skipLoop(boolean doReport) throws Exception {
+ String str;
+ SB ret = (doReport ? new SB() : null);
+ int n = 0;
+ while ((str = (String) peekToken()) != null && str.charAt(0) == '_') {
+ if (ret != null)
+ ret.append(str).append("\n");
+ getTokenPeeked();
+ n++;
+ }
+ if (n == 0)
+ n = columnCount; // end-of-label-section skip
+ int m = 0;
+ while ((str = (String) getNextDataToken()) != null) {
+ if (ret == null)
+ continue;
+ ret.append(str).append(" ");
+ if ((++m % n) == 0)
+ ret.append("\n");
+ }
+ return (ret == null ? null : ret.toString());
+ }
+
+ /**
+ * Get a token as a String value (for the reader)
+ *
+ * @return the next token of any kind, or null
+ * @throws Exception
+ */
+ @Override
+ public String getNextToken() throws Exception {
+ wasUnquoted = true;
+ return (String) getNextTokenProtected();
+ }
+
+ /**
+ * Get the token as a Java Object
+ *
+ * @return the next token of any kind, or null
+ * @throws Exception
+ */
+ public Object getNextTokenObject() throws Exception {
+ wasUnquoted = true;
+ return getNextTokenProtected();
+ }
+
+ /**
+ * Just makes sure
+ * @return String from buffer.
+ * @throws Exception
+ */
+ protected Object getNextTokenProtected() throws Exception {
+ return (getNextLine() ? nextStrToken() : null);
+ }
+
+ /**
+ *
+ * first checks to see if the next token is an unquoted
+ * control code, and if so, returns null
+ *
+ * @return next data token or null
+ * @throws Exception
+ */
+ @Override
+ public Object getNextDataToken() throws Exception {
+ Object o = peekToken();
+ if (o == null)
+ return null;
+ if (wasUnquoted && o instanceof String) {
+ String str = (String) o;
+ if (str.charAt(0) == '_' || str.startsWith("loop_")
+ || str.startsWith("data_")
+ || str.startsWith("save_")
+ || str.startsWith("stop_")
+ || str.startsWith("global_"))
+ return null;
+ }
+ return getTokenPeeked();
+ }
+
+ /**
+ * Just look at the next token. Saves it for retrieval
+ * using getTokenPeeked()
+ *
+ * @return next token or null if EOF
+ * @throws Exception
+ */
+ @Override
+ public Object peekToken() throws Exception {
+ if (!getNextLine())
+ return null;
+ int ich = this.ich;
+ strPeeked = nextStrToken();
+ ichPeeked= this.ich;
+ this.ich = ich;
+ return strPeeked;
+ }
+
+ /**
+ * grab a new line if necessary and prepare it
+ * if it starts with ";"
+ *
+ * @return updated this.str
+ * @throws Exception
+ */
+ private boolean getNextLine() throws Exception {
+ while (!strHasMoreTokens())
+ if (prepareNextLine() == null)
+ return false;
+ return true;
+ }
+
+ /**
+ *
+ * @return the token last acquired; may be null
+ */
+ @Override
+ public Object getTokenPeeked() {
+ ich = ichPeeked;
+ return strPeeked;
+ }
+
+ /**
+ * Used especially for data that might be multi-line data that
+ * might have unwanted white space at start or end.
+ *
+ * @param str
+ * @return str without any leading/trailing white space, and no '\n'
+ */
+ @Override
+ public String fullTrim(String str) {
+ int pt0 = -1;
+ int pt1 = str.length();
+ while (++pt0 < pt1 && PT.isWhitespace(str.charAt(pt0))) {
+ }
+ while (--pt1 > pt0 && PT.isWhitespace(str.charAt(pt1))) {
+ }
+ return str.substring(pt0, pt1 + 1);
+ }
+
+ private final static String grABC =
+ "ABX\u0394E\u03A6\u0393H" // ABCDEFGH
+ + "I_K\u039BMNO\u03A0" // I_KLMNOP
+ + "\u0398P\u03A3TY_\u03A9\u039E\u03A5Z"; // QRSTU_WXYZ
+ private final static String grabc =
+ "\u03B1\u03B2\u03C7\u03A4\u03A5\u03C6\u03B3\u03B7" // abcdefgh
+ + "\u03B9_\u03BA\u03BB\u03BC\u03BD\u03BF\u03C0" // i_klmnop
+ + "\u03B8\u03C1\u03C3\u03C4\u03C5_\u03C9\u03BE\u03C5\u03B6"; // qrstu_wxyz
+
+ /**
+ * Only translating the basic Greek set here, not all the other stuff. See
+ * http://www.iucr.org/resources/cif/spec/version1.1/semantics#markup
+ *
+ * @param data
+ * @return cleaned string
+ */
+ @Override
+ public String toUnicode(String data) {
+ int pt;
+ try {
+ while ((pt = data.indexOf('\\')) >= 0) {
+ int c = data.charAt(pt + 1);
+ String ch = (c >= 65 && c <= 90 ? grABC.substring(c - 65, c - 64)
+ : c >= 97 && c <= 122 ? grabc.substring(c - 97, c - 96) : "_");
+ data = data.substring(0, pt) + ch + data.substring(pt + 2);
+ }
+ } catch (Exception e) {
+ // ignore
+ }
+
+ return data;
+ }
+
+ /**
+ * Process a data block, with or without a loop_.
+ *
+ * Passed an array of field names, this method fills two int[] arrays. The
+ * first, key2col, maps desired key values to actual order of appearance
+ * (column number) in the file; the second, col2key, is a reverse loop-up for
+ * that, mapping column numbers to desired field indices.
+ *
+ * When called within a loop_ context, this.columnData will be created but not filled.
+ *
+ * Alternatively, if fields is null, then this.fieldNames is
+ * filled, in order, with key data, and both key2col and col2key will be
+ * simply 0,1,2,... This array is used in cases such as matrices for which
+ * there are simply too many possibilities to list, and the key name itself
+ * contains information that we need.
+ *
+ * When not a loop_ context, keys are expected to be in the mmCIF form
+ * category.subkey and will be unique within a data block (see
+ * http://mmcif.wwpdb.org/docs/tutorials/mechanics/pdbx-mmcif-syntax.html).
+ * Keys and data will be read for all data in the same category, filling this.columnData.
+ *
+ *
+ * In this way, the calling class does not need to enumerate all possible
+ * category names, but instead can focus on just those of interest.
+ *
+ *
+ * @param fields
+ * list of normalized field names, such as
+ * "_pdbx_struct_assembly_gen_assembly_id" (with "_" instead of ".")
+ * @param key
+ * null to indicate a loop_ construct, otherwise the initial category.subkey
+ * found
+ * @param data
+ * when not loop_ the initial data read, otherwise ignored
+ * @param key2col
+ * map of desired keys to actual columns
+ * @param col2key
+ * map of actual columns to desired keys
+ * @throws Exception
+ */
+ @Override
+ public void parseDataBlockParameters(String[] fields, String key,
+ String data, int[] key2col, int[] col2key) throws Exception {
+ isLoop = (key == null);
+ Object o;
+ String s;
+ if (fields == null) {
+ // for reading full list of keys, as for matrices
+ columnNames = new String[KEY_MAX];
+ } else {
+ if (!htFields.containsKey(fields[0]))
+ for (int i = fields.length; --i >= 0;)
+ htFields.put(fields[i], Integer.valueOf(i));
+ for (int i = fields.length; --i >= 0;)
+ key2col[i] = NONE;
+ }
+ columnCount = 0;
+ int pt, i;
+ if (isLoop) {
+ while (true) {
+ o = peekToken();
+ if (o == null) {
+ // we are PREMATURELY done; reset
+ columnCount = 0;
+ break;
+ }
+ // end of the loop is a new token not starting with underscore
+ if (!(o instanceof String) || ((String) o).charAt(0) != '_')
+ break;
+
+ pt = columnCount++;
+ s = fixKey((String) getTokenPeeked());
+ if (fields == null) {
+ // just make a linear model, saving the list
+ columnNames[col2key[pt] = key2col[pt] = pt] = s;
+ continue;
+ }
+ Integer iField = htFields.get(s);
+ i = (iField == null ? NONE : iField.intValue());
+ if ((col2key[pt] = i) != NONE)
+ key2col[i] = pt;
+ }
+ } else {
+ pt = key.indexOf(".");
+ String str0 = (pt < 0 ? key : key.substring(0, pt + 1));
+ while (true) {
+ // end of the loop is a new token not starting with underscore
+ pt = columnCount++;
+ if (key == null) {
+ key = (String) getTokenPeeked();
+ data = getNextToken();
+ }
+ Integer iField = htFields.get(fixKey(key));
+ i = (iField == null ? NONE : iField.intValue());
+ if ((col2key[pt] = i) != NONE)
+ columnData[key2col[i] = pt] = data;
+ if ((o = peekToken()) == null || !(o instanceof String) || !((String) o).startsWith(str0))
+ break;
+ key = null;
+ }
+ haveData = (columnCount > 0);
+ }
+ }
+
+ @Override
+ public String fixKey(String key) {
+ // PRELIMINARY -- BilBao _magnetic
+ // PRELIMINARY -- Jana2006
+ return (
+ key.startsWith("_magnetic") ? key.substring(9)
+ : key.startsWith("_jana") ? key.substring(5)
+ : key).replace('.', '_').toLowerCase();
+ }
+
+ //////////////////// private methods ////////////////////
+
+
+ /**
+ * sets global str and line to be parsed from the beginning
+ *
+ * \1 .... \1 indicates an embedded fully escaped data object
+ *
+ * @param str new data string
+ * @return str
+ */
+ protected String setString(String str) {
+ this.str = line = str;
+ cch = (str == null ? 0 : str.length());
+ ich = 0;
+ return str;
+ }
+
+ /*
+ * http://www.iucr.org/resources/cif/spec/version1.1/cifsyntax
+ *
+ * 17. The special sequence of end-of-line followed
+ * immediately by a semicolon in column one (denoted "<eol>;")
+ * may also be used as a delimiter at the beginning and end
+ * of a character string comprising a data value. The complete
+ * bounded string is called a text field, and may be used to
+ * convey multi-line values. The end-of-line associated with
+ * the closing semicolon does not form part of the data value.
+ * Within a multi-line text field, leading white space within
+ * text lines must be retained as part of the data value; trailing
+ * white space on a line may however be elided.
+ *
+ * 18. A text field delimited by the <eol>; digraph may not
+ * include a semicolon at the start of a line of text as
+ * part of its value.
+ *
+ * 20. For example, the data value foo may be expressed
+ * equivalently as an unquoted string foo, as a quoted
+ * string 'foo' or as a text field
+ *
+ *;foo
+ *;
+ *
+ * By contrast the value of the text field
+ *
+ *; foo
+ * bar
+ *;
+ *
+ * is foo<eol> bar (where <eol> represents an end-of-line);
+ * the embedded space characters are significant.
+ *
+ *
+ * I (BH) note, however, that we sometimes have:
+ *
+ * _some_name
+ * ;
+ * the name here
+ * ;
+ *
+ * so this should actually be
+ *
+ * ;the name here
+ * ;
+ *
+ * for this, we use fullTrim();
+ *
+ */
+
+ /**
+ *
+ * sets the string for parsing to be from the next line
+ * when the token buffer is empty, and if ';' is at the
+ * beginning of that line, extends the string to include
+ * that full multiline string. Uses \1 to indicate that
+ * this is a special quotation.
+ *
+ *
+ *
+ * @return the next line or null if EOF
+ * @throws Exception
+ */
+ protected String prepareNextLine() throws Exception {
+ setString(readLine());
+ if (line == null || line.length() == 0)
+ return line;
+ if (line.charAt(0) == ';')
+ return preprocessString();
+ if (str.startsWith("###non-st#"))
+ ich = 10;
+ return line;
+ }
+
+ /**
+ * Preprocess the string on a line starting with a semicolon
+ * to produce a string with a \1 ... \1 segment
+ * that will be picked up in the next round
+ *
+ * @return escaped part with attached extra data
+ * @throws Exception
+ */
+ protected String preprocessString() throws Exception {
+ return setString(preprocessSemiString());
+ }
+
+ /**
+ * Encapsulate a multi-line ; .... ; string with \1 ... \1
+ *
+ * CIF 1.0 and CIF 2.0
+ *
+ * @return ecapsulated string
+ * @throws Exception
+ */
+ protected String preprocessSemiString() throws Exception {
+ ich = 1;
+ String str = '\1' + line.substring(1) + '\n';
+ while (readLine() != null) {
+ if (line.startsWith(";")) {
+ // remove trailing <eol> only, and attach rest of next line
+ str = str.substring(0, str.length() - 1)
+ + '\1' + line.substring(1);
+ break;
+ }
+ str += line + '\n';
+ }
+ return str;
+ }
+
+ /**
+ * @return TRUE if there are more tokens in the line buffer
+ *
+ */
+ private boolean strHasMoreTokens() {
+ if (str == null)
+ return false;
+ char ch = '#';
+ while (ich < cch && ((ch = str.charAt(ich)) == ' ' || ch == '\t'))
+ ++ich;
+ return (ich < cch && ch != '#');
+ }
+
+ /**
+ * assume that hasMoreTokens() has been called and that ich is pointing at a
+ * non-white character. Also sets boolean wasUnQuoted, because we need to know
+ * if we should be checking for a control keyword. 'loop_' is different from
+ * just loop_ without the quotes.
+ *
+ * @return null if no more tokens, "\0" if '.' or '?', or next token
+ */
+ private Object nextStrToken() {
+ if (ich == cch)
+ return null;
+ char ch = str.charAt(ich);
+ if (isQuote(ch)) {
+ wasUnquoted = false;
+ return getQuotedStringOrObject(ch);
+ }
+ int ichStart = ich;
+ wasUnquoted = true;
+ while (ich < cch && !isTerminator(ch = str.charAt(ich)))
+ ++ich;
+ if (ich == ichStart + 1)
+ if (nullString != null
+ && (str.charAt(ichStart) == '.' || str.charAt(ichStart) == '?'))
+ return nullString;
+ String s = str.substring(ichStart, ich);
+ return unquoted(s);
+ }
+
+ /**
+ * In CIF 2.0, this method turns a String into an Integer or Float
+ * In CIF 1.0 (here) just return the unchanged value.
+ * @param s unquoted string
+ * @return unchanged value
+ */
+ protected Object unquoted(String s) {
+ return s;
+ }
+
+ /**
+ * The token terminator is space or tab in CIF 1.0,
+ * but it can be quoted strings in CIF 2.0.
+ *
+ * @param c
+ * @return true if this character is a terminator
+ */
+ protected boolean isTerminator(char c) {
+ return c == ' ' || c == '\t' || c == cterm ;
+ }
+
+ /**
+ * CIF 1.0 only; we handle various quote types here
+ * @param ch
+ * @return true if this character is a (starting) quote
+ */
+ protected boolean isQuote(char ch) {
+ switch (ch) {
+ case '\'':
+ case '\"':
+ case '\1':
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * CIF 1.0 only.
+ *
+ *
+ * @param ch current character being pointed to
+ * @return a String data object
+ */
+ protected Object getQuotedStringOrObject(char ch) {
+ int ichStart = ich;
+ char chClosingQuote = ch;
+ boolean wasQuote = false;
+ while (++ich < cch) {
+ ch = str.charAt(ich);
+ // CIF 1.0 rules require that the closing ' or "" be followed by space or tab or EOL
+ if (wasQuote && (ch == ' ' || ch == '\t'))
+ break;
+ wasQuote = (ch == chClosingQuote);
+ }
+ int pt1 = ichStart + 1;
+ int pt2 = ich - 1;
+ if (ich == cch && !wasQuote) {
+ // reached the end of the string without finding closing '
+ // so take the whole thing. Probably a bad CIF file.
+ pt1--;
+ pt2++;
+ } else {
+ // throw away the last white character
+ ++ich;
+ }
+ return str.substring(pt1, pt2);
+ }
+
+
+}
\ No newline at end of file