public class MonomerException extends Exception {

	/**
	 * Creates a new instance of <code>MonomerException</code> without detail
	 * message.
	 */
	public MonomerException() {
	}

	/**
	 * Constructs an instance of <code>MonomerException</code> with the
	 * specified detail message.
	 * 
	 * @param msg
	 *            the detail message.
	 */
	public MonomerException(String msg) {
		super(msg);
	}

	public MonomerException(String msg, Throwable err) {
		super(msg, err);
	}
} public class MonomerFactory {

	public static final String NOTATION_DIRECTORY = NotationConstant.NOTATION_DIRECTORY;
	public static final String MONOMER_CACHE_FILE_NAME = "MonomerCache.ser";
	public static final String MONOMER_CACHE_FILE_PATH = NOTATION_DIRECTORY
			+ System.getProperty("file.separator") + MONOMER_CACHE_FILE_NAME;
	public static final String MONOMER_DB_FILE_NAME = "MonomerDBGZEncoded.xml";
	public static final String MONOMER_DB_FILE_PATH = NOTATION_DIRECTORY
			+ System.getProperty("file.separator") + MONOMER_DB_FILE_NAME;
	public static final String MONOMER_DB_XML_RESOURCE = "resources/MonomerDBGZEncoded.xml";
	public static final String MONOMER_DB_SCHEMA_RESOURCE = "resources/MonomerDBSchema.xsd";
	public static final String XML_SCHEMA_VALIDATION_FEATURE = "http://apache.org/xml/features/validation/schema";
	public static final String EXTERNAL_SCHEMA_LOCATION_KEY = "http://apache.org/xml/properties/schema/external-schemaLocation";
	public static final String DEFAULT_NAME_SPACE = "lmr";
	public static final String POLYMER_LIST_ELEMENT = "PolymerList";
	public static final String POLYMER_ELEMENT = "Polymer";
	public static final String POLYMER_TYPE_ATTRIBUTE = "polymerType";
	public static final String ATTACHMENT_LIST_ELEMENT = "AttachmentList";
	private static MonomerFactory instance;
	/**
	 * First key is polymer Type, such as "RNA" Second key is monomer ID, such
	 * as "A"
	 */
	private static Map<String, Map<String, Monomer>> monomerDB; // key is
																// monomer
																// SMILES, value
																// is Monomer
	private static Map<String, Monomer> smilesMonomerDB; // key is
															// AttachementID,
															// value is
															// Attachment
	private static Map<String, Attachment> attachmentDB;
	// private static Map<String, Map<String, Monomer>> externalMonomerDB;
	private static SAXBuilder builder;
	private static Logger logger = Logger.getLogger(MonomerFactory.class
			.toString());

	private static boolean dbChanged = true;

	/**
	 * retruns the monomer database
	 * 
	 * @return Map as Map<String, Map<String, Monomer>>
	 */
	public synchronized Map<String, Map<String, Monomer>> getMonomerDB() {
		return getMonomerDB(true);
	}

	/**
	 * returns the monomer database including monomers that where temporary
	 * marked as new, else without those monomers
	 * 
	 * @param includeNewMonomers
	 * @return Map as Map<String, Map<String, Monomer>>
	 */
	public synchronized Map<String, Map<String, Monomer>> getMonomerDB(
			boolean includeNewMonomers) {
		if (includeNewMonomers) {
			return monomerDB;
		} else {
			Map<String, Map<String, Monomer>> reducedMonomerDB = new HashMap<String, Map<String, Monomer>>();
			for (String polymerType : monomerDB.keySet()) {
				Map<String, Monomer> monomerMap = monomerDB.get(polymerType);
				reducedMonomerDB.put(polymerType,
						excludeNewMonomers(monomerMap));
			}
			return reducedMonomerDB;
		}
	}

	protected MonomerStore monomerStore;

	/**
	 * create a MonomerStore instance based on MonomerFactory's monomerDB and
	 * smilesMonomerDB
	 * 
	 * @return MonomerStore
	 */
	public synchronized MonomerStore getMonomerStore() {
		if (monomerStore == null) {
			monomerStore = new MonomerStore(monomerDB, smilesMonomerDB);
		}
		return monomerStore;
	}

	public synchronized Map<String, Attachment> getAttachmentDB() {
		return attachmentDB;
	}

	public synchronized Map<String, Monomer> getSmilesMonomerDB() {
		return getSmilesMonomerDB(true);
	}

	public synchronized Map<String, Monomer> getSmilesMonomerDB(
			boolean includeNewMonomers) {
		if (includeNewMonomers) {
			return smilesMonomerDB;
		} else {
			return excludeNewMonomers(smilesMonomerDB);
		}
	}

	private synchronized Map<String, Monomer> excludeNewMonomers(
			Map<String, Monomer> monomerMap) {
		Map<String, Monomer> reducedMonomerMap = new HashMap<String, Monomer>();
		for (String identifier : monomerMap.keySet()) {
			Monomer monomer = monomerMap.get(identifier);
			if (!monomer.isNewMonomer()) {
				reducedMonomerMap.put(identifier, monomer);
			}
		}
		return reducedMonomerMap;
	}

	public synchronized List<String> getPolymerTypes() {
		List<String> l = new ArrayList<String>();
		l.addAll(monomerDB.keySet());
		Collections.sort(l);
		return l;
	}

	public synchronized List<String> getMonomerTypes() {
		List<String> monomerTypeList = new ArrayList<String>();
		Object[] col = monomerDB.values().toArray();
		for (int i = 0; i < col.length; i++) {
			Map<String, Monomer> map = (Map<String, Monomer>) col[i];
			Monomer[] monomers = map.values().toArray(new Monomer[0]);
			for (int j = 0; j < monomers.length; j++) {
				if (!monomerTypeList.contains(monomers[j].getMonomerType())) {
					monomerTypeList.add(monomers[j].getMonomerType());
				}
			}
		}
		Collections.sort(monomerTypeList);
		return monomerTypeList;
	}

	public synchronized Map<String, List<String>> getAttachmentLabelIDs() {
		Map<String, List<String>> labelMap = new HashMap<String, List<String>>();

		// group attachments based on R value (label)
		Set<String> idSet = attachmentDB.keySet();
		for (String id : idSet) {
			String label = attachmentDB.get(id).getLabel();
			List<String> ids = labelMap.get(label);
			if (null == ids || ids.isEmpty()) {
				ids = new ArrayList<String>();
				ids.add(id);
				labelMap.put(label, ids);
			} else {
				ids.add(id);
			}
		}

		// Sort ids in each R group
		Set<String> labelSet = labelMap.keySet();
		for (String label : labelSet) {
			List<String> ids = labelMap.get(label);
			Collections.sort(ids);
		}

		return labelMap;
	}

	public static void setupBuilder() {
		URL schema = MonomerFactory.class
				.getResource(MONOMER_DB_SCHEMA_RESOURCE);
		builder = new SAXBuilder(true); // checks both well-formedness and
										// validity
		builder.setFeature(XML_SCHEMA_VALIDATION_FEATURE, true);
		builder.setProperty(EXTERNAL_SCHEMA_LOCATION_KEY, DEFAULT_NAME_SPACE
				+ " " + schema.toString());
	}

	private MonomerFactory() {
	}

	/**
	 * Initialize MonomerCache and returns the singlton Factory class
	 * 
	 * @return MonomerFactory
	 * @throws org.helm.notation.MonomerException
	 * @throws java.io.IOException
	 * @throws org.jdom.JDOMException
	 */
	public static MonomerFactory getInstance() throws MonomerException,
			IOException, JDOMException {
		if (null == instance) {
			initializeMonomerCache();
			instance = new MonomerFactory();
		}
		return instance;
	}

	public static void setDBChanged(boolean isChanged) {
		dbChanged = isChanged;
	}

	/**
	 * Returns whether one of the stored databases has changed, for example by
	 * adding or removing monomers.
	 * 
	 * @return true when database has changed else false
	 */
	public static boolean hasDBChanged() {
		return dbChanged;
	}

	public static void resetDBChanged() {
		dbChanged = false;
	}

	private static void serializeMonomerCache(MonomerCache monomerCache,
			String fileName) throws IOException {
		FileOutputStream fos = new FileOutputStream(fileName);
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		oos.writeObject(monomerCache);
		oos.close();
		fos.close();
	}

	private static MonomerCache deserializeMonomerCache(String fileName)
			throws IOException, MonomerException {
		FileInputStream fis = new FileInputStream(fileName);
		ObjectInputStream ois = new ObjectInputStream(fis);
		MonomerCache cache = null;
		try {
			cache = (MonomerCache) ois.readObject();
			// ensure monomers have their canonical SMILES set to what we store
			for (Map.Entry<String,Monomer> e : cache.getSmilesMonomerDB().entrySet()) {
				e.getValue().setCanSMILES(e.getKey());
			}
		} catch (ClassNotFoundException cnfe) {
			throw new MonomerException(
					"Unable to deserialize monomer cache from file");
		} finally {
			ois.close();
		}
		fis.close();
		return cache;
	}

	/**
	 * To add new monomer into monomerCache
	 * 
	 * @param monomer
	 */
	public synchronized void addNewMonomer(Monomer monomer) throws IOException,
			MonomerException {
		monomer.setNewMonomer(true);
		addMonomer(monomerDB, smilesMonomerDB, monomer);

		dbChanged = true;
	}

	private void addMonomer(Map<String, Map<String, Monomer>> monomerDB,
			Map<String, Monomer> smilesMonomerDB, Monomer monomer)
			throws IOException, MonomerException {
		Map<String, Monomer> monomerMap = monomerDB.get(monomer
				.getPolymerType());

		if (null == monomerMap) {
			Map<String, Monomer> map = new HashMap<String, Monomer>();
			Monomer copyMonomer = DeepCopy.copy(monomer);
			map.put(monomer.getAlternateId(), copyMonomer);
			monomerDB.put(monomer.getPolymerType(), map);
		} else {
			if (!monomerMap.containsKey(monomer.getAlternateId())) {
				monomerMap.put(monomer.getAlternateId(), monomer);
			}
		}

		if (monomer.getCanSMILES() != null
				&& monomer.getCanSMILES().length() > 0) {
			if (!smilesMonomerDB.containsKey(monomer.getCanSMILES())) {
				smilesMonomerDB.put(monomer.getCanSMILES(), monomer);
			}
		}

		dbChanged = true;
	}

	/**
	 * Build an MonomerCache object with monomerDBXML String
	 * 
	 * @param monomerDBXML
	 * @return MonomerCache
	 * @throws org.helm.notation.MonomerException
	 * @throws java.io.IOException
	 * @throws org.jdom.JDOMException
	 */
	public MonomerCache buildMonomerCacheFromXML(String monomerDBXML)
			throws MonomerException, IOException, JDOMException,
			StructureException {
		ByteArrayInputStream bais = new ByteArrayInputStream(
				monomerDBXML.getBytes());
		return buildMonomerCacheFromXML(bais);
	}

	/**
	 * merge remote monomerCache with local monomerCache, will throw exception
	 * if conflicts found. Client needs to resolve conflicts prior to calling
	 * merge
	 * 
	 * @param remoteMonomerCache
	 * @throws java.io.IOException
	 * @throws org.helm.notation.MonomerException
	 */
	public synchronized void merge(MonomerCache remoteMonomerCache)
			throws IOException, MonomerException {
		Map<Monomer, Monomer> conflicts = getConflictedMonomerMap(remoteMonomerCache);
		if (conflicts.size() > 0) {
			throw new MonomerException(
					"Local new monomer and remote monomer database conflict found");
		} else {
			Map<String, Map<String, Monomer>> monoDB = remoteMonomerCache
					.getMonomerDB();

			Set<String> polymerTypeSet = monoDB.keySet();
			for (Iterator i = polymerTypeSet.iterator(); i.hasNext();) {
				String polymerType = (String) i.next();
				Map<String, Monomer> map = monoDB.get(polymerType);
				Set<String> monomerSet = map.keySet();
				for (Iterator it = monomerSet.iterator(); it.hasNext();) {
					String id = (String) it.next();
					Monomer m = map.get(id);
					addMonomer(monomerDB, smilesMonomerDB, m);
				}
			}
		}

		dbChanged = true;
	}

	/**
	 * replace local cache with remote one completely, may cause loss of data
	 * 
	 * @param remoteMonomerCache
	 * @throws java.io.IOException
	 * @throws org.helm.notation.MonomerException
	 */
	public synchronized void setMonomerCache(MonomerCache remoteMonomerCache)
			throws IOException, MonomerException {
		monomerDB = remoteMonomerCache.getMonomerDB();
		attachmentDB = remoteMonomerCache.getAttachmentDB();
		smilesMonomerDB = remoteMonomerCache.getSmilesMonomerDB();

		dbChanged = true;
	}

	/**
	 * 
	 * @param remoteMonomerCache
	 * @return localMonomer and remoteMonomer mismatch, key is local, value is
	 *         remote
	 * @throws java.io.IOException
	 * @throws org.helm.notation.MonomerException
	 */
	public synchronized Map<Monomer, Monomer> getConflictedMonomerMap(
			MonomerCache remoteMonomerCache) throws IOException,
			MonomerException {
		Map<String, Map<String, Monomer>> remoteMonomerDB = remoteMonomerCache
				.getMonomerDB();
		Map<String, Monomer> remoteSmilesDB = remoteMonomerCache
				.getSmilesMonomerDB();

		Map<Monomer, Monomer> map = new HashMap<Monomer, Monomer>();
		List<Monomer> newMonomers = getNewMonomers(monomerDB);
		if (newMonomers.size() > 0) {

			for (int i = 0; i < newMonomers.size(); i++) {
				Monomer local = newMonomers.get(i);
				if (remoteMonomerDB.containsKey(local.getPolymerType())) {
					Map<String, Monomer> monomers = remoteMonomerDB.get(local
							.getPolymerType());

					if (monomers.containsKey(local.getAlternateId())) {
						Monomer remote = monomers.get(local.getAlternateId());
						if (local.getCanSMILES().equals(remote.getCanSMILES())) {
							logger.log(Level.INFO, "Perfect Match");
						} else {
							map.put(local, remote);
						}
					} else {
						if (remoteSmilesDB.containsKey(local.getCanSMILES())) {
							Monomer remote = remoteSmilesDB.get(local
									.getCanSMILES());
							map.put(local, remote);
						} else {
							logger.log(Level.INFO, "Really New");
						}
					}
				} else {
					logger.log(Level.INFO, "New Polymer Type");
				}
			}
		}
		return map;
	}

	private List<Monomer> getNewMonomers(
			Map<String, Map<String, Monomer>> monomerDB) {
		List<Monomer> l = new ArrayList<Monomer>();
		Set<String> typeSet = monomerDB.keySet();
		for (Iterator<String> i = typeSet.iterator(); i.hasNext();) {
			String polymerType = (String) i.next();
			Map<String, Monomer> map = (Map<String, Monomer>) monomerDB
					.get(polymerType);
			Object[] monomers = map.values().toArray();
			for (int j = 0; j < monomers.length; j++) {
				Monomer m = (Monomer) monomers[j];
				if (m.isNewMonomer()) {
					l.add(m);
				}
			}
		}
		return l;
	}

	private static MonomerCache buildMonomerCacheFromXML(
			InputStream monomerDBInputStream) throws MonomerException,
			IOException, JDOMException {
		if (null == builder) {
			setupBuilder();
		}
		Document doc = builder.build(monomerDBInputStream);
		Element root = doc.getRootElement();

		Element polymerList = root.getChild(POLYMER_LIST_ELEMENT,
				root.getNamespace());
		Element attachmentList = root.getChild(ATTACHMENT_LIST_ELEMENT,
				root.getNamespace());

		Map<String, Map<String, Monomer>> newMonomerDB = buildMonomerDB(polymerList);
		Map<String, Attachment> newAttachmentDB = buildAttachmentDB(attachmentList);
		Map<String, Monomer> newSmilesMonomerDB = buildSmilesMonomerDB(newMonomerDB);

		MonomerCache cache = new MonomerCache();
		cache.setMonomerDB(newMonomerDB);
		cache.setAttachmentDB(newAttachmentDB);
		cache.setSmilesMonomerDB(newSmilesMonomerDB);

		return cache;
	}

	private static String buildMonomerDbXMLFromCache(MonomerCache cache)
			throws MonomerException {
		XMLOutputter outputer = new XMLOutputter(Format.getCompactFormat());

		StringBuilder sb = new StringBuilder();
		sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?><MonomerDB xmlns=\"lmr\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">");

		Map<String, Map<String, Monomer>> mDB = cache.getMonomerDB();
		Element polymerListElement = new Element(POLYMER_LIST_ELEMENT);
		Set<String> polymerTypeSet = mDB.keySet();
		for (Iterator i = polymerTypeSet.iterator(); i.hasNext();) {
			String polymerType = (String) i.next();
			Element polymerElement = new Element(POLYMER_ELEMENT);
			Attribute att = new Attribute(POLYMER_TYPE_ATTRIBUTE, polymerType);
			polymerElement.setAttribute(att);
			polymerListElement.getChildren().add(polymerElement);

			Map<String, Monomer> monomerMap = mDB.get(polymerType);
			Set<String> monomerSet = monomerMap.keySet();

			for (Iterator it = monomerSet.iterator(); it.hasNext();) {
				String monomerID = (String) it.next();
				Monomer m = monomerMap.get(monomerID);
				Element monomerElement = MonomerParser.getMonomerElement(m);
				polymerElement.getChildren().add(monomerElement);
			}
		}
		String polymerListString = outputer.outputString(polymerListElement);
		sb.append(polymerListString);

		Map<String, Attachment> aDB = cache.getAttachmentDB();
		Element attachmentListElement = new Element(ATTACHMENT_LIST_ELEMENT);
		Set<String> attachmentSet = aDB.keySet();
		for (Iterator itr = attachmentSet.iterator(); itr.hasNext();) {
			String attachmentID = (String) itr.next();
			Attachment attachment = aDB.get(attachmentID);
			Element attachmentElement = MonomerParser
					.getAttachementElement(attachment);
			attachmentListElement.getChildren().add(attachmentElement);
		}
		String attachmentListString = outputer
				.outputString(attachmentListElement);
		sb.append(attachmentListString);

		sb.append("</MonomerDB>");

		return sb.toString();
	}

	/**
	 * This method is called during startup, use serialized version if exists,
	 * otherwise use XML version (First from local, then from jar)
	 * 
	 * @throws org.helm.notation.MonomerException
	 * @throws java.io.IOException
	 * @throws org.jdom.JDOMException
	 */
	private static void initializeMonomerCache() throws MonomerException,
			IOException, JDOMException {
		MonomerCache cache = null;
		InputStream in = null;

		File cacheFile = new File(MONOMER_CACHE_FILE_PATH);
		if (cacheFile.exists()) {
			try {
				cache = deserializeMonomerCache(MONOMER_CACHE_FILE_PATH);
				validate(cache.getMonomerDB());
				logger.log(Level.INFO, MONOMER_CACHE_FILE_PATH
						+ " is used for monomer cache initialization");
			} catch (Exception e) {
				logger.log(Level.INFO,
						"Unable to use local monomer cache file: "
								+ MONOMER_CACHE_FILE_NAME);
				cacheFile.delete();
				logger.log(Level.INFO, "Deleted local monomer cache file: "
						+ MONOMER_CACHE_FILE_NAME);
			}
		}

		File localMonomerDBFile = new File(MONOMER_DB_FILE_PATH);
		if (null == cache && localMonomerDBFile.exists()) {
			try {
				in = new FileInputStream(MONOMER_DB_FILE_PATH);
				cache = buildMonomerCacheFromXML(in);
				validate(cache.getMonomerDB());
				logger.log(Level.INFO, MONOMER_DB_FILE_PATH
						+ " is used for monomer cache initialization");
			} catch (Exception e) {
				logger.log(Level.INFO, "Unable to use local monomer DB file: "
						+ MONOMER_DB_FILE_NAME);
				localMonomerDBFile.delete();
				logger.log(Level.INFO, "Deleted local monomer DB file: "
						+ MONOMER_DB_FILE_NAME);
			}
		}

		if (null == cache) {
			in = MonomerFactory.class
					.getResourceAsStream(MONOMER_DB_XML_RESOURCE);
			cache = buildMonomerCacheFromXML(in);
			validate(cache.getMonomerDB());

			logger.log(Level.INFO, MONOMER_DB_XML_RESOURCE
					+ " is used for monomer cache initialization");
		}

		monomerDB = cache.getMonomerDB();
		attachmentDB = cache.getAttachmentDB();
		smilesMonomerDB = cache.getSmilesMonomerDB();

		dbChanged = true;
	}

	/**
	 * save monomerCache to disk file
	 * 
	 * @throws java.io.IOException
	 */
	public void saveMonomerCache() throws IOException, MonomerException {
		File f = new File(NOTATION_DIRECTORY);
		if (!f.exists()) {
			f.mkdir();
		}
		MonomerCache cache = new MonomerCache();
		cache.setMonomerDB(getMonomerDB(false));
		cache.setAttachmentDB(getAttachmentDB());
		cache.setSmilesMonomerDB(getSmilesMonomerDB(false));
		serializeMonomerCache(cache, MONOMER_CACHE_FILE_PATH);

		String monomerDbXML = buildMonomerDbXMLFromCache(cache);
		FileOutputStream fos = new FileOutputStream(MONOMER_DB_FILE_PATH);
		fos.write(monomerDbXML.getBytes());
	}

	private static Map<String, Map<String, Monomer>> buildMonomerDB(
			Element polymerList) throws MonomerException, IOException,
			JDOMException {
		Map<String, Map<String, Monomer>> map = new HashMap<String, Map<String, Monomer>>();
		List poplymers = polymerList.getChildren();

		Iterator i = poplymers.iterator();
		while (i.hasNext()) {
			Element polymer = (Element) i.next();
			Attribute polymerType = polymer
					.getAttribute(POLYMER_TYPE_ATTRIBUTE);

			Map idMonomerMap = new HashMap<String, Monomer>();

			List monomers = polymer.getChildren();
			Iterator it = monomers.iterator();
			while (it.hasNext()) {
				Element monomer = (Element) it.next();
				Monomer m = MonomerParser.getMonomer(monomer);
				if (MonomerParser.validateMonomer(m)) {
					idMonomerMap.put(m.getAlternateId(), m);
				}
			}
			map.put(polymerType.getValue(), idMonomerMap);
		}

		return map;
	}

	private static Map<String, Attachment> buildAttachmentDB(
			Element attachmentList) throws MonomerException, IOException,
			JDOMException {
		Map<String, Attachment> map = new HashMap<String, Attachment>();

		List attachments = attachmentList.getChildren();
		Iterator i = attachments.iterator();
		while (i.hasNext()) {
			Element attachment = (Element) i.next();
			Attachment att = MonomerParser.getAttachment(attachment);

			if (MonomerParser.validateAttachement(att)) {
				map.put(att.getAlternateId(), att);
			}

		}

		return map;
	}

	private static Map<String, Monomer> buildSmilesMonomerDB(
			Map<String, Map<String, Monomer>> monomerDB) {
		Map<String, Monomer> map = new HashMap<String, Monomer>();
		Set<String> polymerSet = monomerDB.keySet();
		for (Iterator i = polymerSet.iterator(); i.hasNext();) {
			String polymer = (String) i.next();
			Map<String, Monomer> monomerMap = monomerDB.get(polymer);
			Set<String> monomerSet = monomerMap.keySet();
			for (Iterator it = monomerSet.iterator(); it.hasNext();) {
				String monomerID = (String) it.next();
				Monomer monomer = monomerMap.get(monomerID);
				String smiles = monomer.getCanSMILES();
				try {
					smiles = StructureParser.getUniqueExtendedSMILES(smiles);
				} catch (Exception e) {
					// ignored
				}
				monomer.setCanSMILES(smiles);
				map.put(smiles, monomer);

			}
		}
		// System.out.println("smiles map count"+map.size());
		return map;
	}

	private static boolean validate(Map<String, Map<String, Monomer>> monomerDB)
			throws MonomerException, IOException {
		Set<String> polymers = monomerDB.keySet();
		for (String polymer : polymers) {
			Map<String, Monomer> monomerMap = monomerDB.get(polymer);
			Set<String> monomers = monomerMap.keySet();
			for (String monomer : monomers) {
				Monomer m = monomerMap.get(monomer);
				MonomerParser.validateMonomer(m);
			}
		}
		return true;
	}

	public static void finalizeMonomerCache() {

		monomerDB = null;
		attachmentDB = null;
		smilesMonomerDB = null;
		dbChanged = true;
		instance = null;
	}

} public class MonomerStore {
	private Map<String, Map<String, Monomer>> monomerDB;
	private Map<String, Monomer> smilesMonomerDB;

	/**
	 * Constructor with Monomer- and SmilesDB
	 * 
	 * @param monomerDB
	 * @param smilesMonomerDB
	 */
	public MonomerStore(Map<String, Map<String, Monomer>> monomerDB,
			Map<String, Monomer> smilesMonomerDB) {
		this.monomerDB = monomerDB;
		this.smilesMonomerDB = smilesMonomerDB;
	}

	/**
	 * 
	 * Constructs empty MonomerStore
	 * 
	 */
	public MonomerStore() {
		monomerDB = new HashMap<String, Map<String, Monomer>>();
		smilesMonomerDB = new HashMap<String, Monomer>();
	}

	/**
	 * returns MonomerDB
	 * 
	 * @return MonomerDB as Map<String, Map<String, Monomer>>
	 */
	public Map<String, Map<String, Monomer>> getMonomerDB() {
		return monomerDB;
	}

	/**
	 * returns SmilesMonomerDB
	 * 
	 * @return SmilesMonomerDB as Map<String, Monomer>
	 */
	public Map<String, Monomer> getSmilesMonomerDB() {
		return smilesMonomerDB;
	}

	/**
	 * Adds a monomer to the store
	 * 
	 * @param monomer
	 * @throws IOException
	 * @throws MonomerException
	 */
	public void addMonomer(Monomer monomer) throws IOException,
			MonomerException {
		addMonomer(monomer, false);
	}

	/**
	 * Adds a monomer to the store and optionally sets the dbChanged flag
	 * 
	 * @param monomer
	 * @param dbChanged
	 * @throws IOException
	 * @throws MonomerException
	 */
	public void addMonomer(Monomer monomer, boolean dbChanged)
			throws IOException, MonomerException {
		Map<String, Monomer> monomerMap = monomerDB.get(monomer
				.getPolymerType());
		String polymerType = monomer.getPolymerType();
		String alternateId = monomer.getAlternateId();
		String smilesString = monomer.getCanSMILES();

		try {
			smilesString = StructureParser
					.getUniqueExtendedSMILES(smilesString);
		} catch (Exception e) {
			smilesString = monomer.getCanSMILES();
		}

		boolean hasSmilesString = (smilesString != null && smilesString
				.length() > 0);

		if (null == monomerMap) {
			monomerMap = new HashMap<String, Monomer>();
			monomerDB.put(polymerType, monomerMap);
		}

		Monomer copyMonomer = DeepCopy.copy(monomer);
		
		// ensure the canonical SMILES is indexed in the monomer store
		if (hasSmilesString) {
			copyMonomer.setCanSMILES(smilesString);
		}

		boolean alreadyAdded = false;
		alreadyAdded = monomerMap.containsKey(alternateId);

		if (!alreadyAdded) {
			monomerMap.put(alternateId, copyMonomer);

			boolean alreadyInSMILESMap = hasSmilesString
					&& (smilesMonomerDB.containsKey(smilesString));

			if (!alreadyInSMILESMap) {
				smilesMonomerDB.put(smilesString, copyMonomer);
			}
		}

		if (dbChanged) {
			MonomerFactory.setDBChanged(true);
		}
	}

	/**
	 * Checks if a specific monomer exists in the store
	 * 
	 * @param polymerType
	 * @param alternateId
	 * @return true if monomer exists, false if not
	 */
	public boolean hasMonomer(String polymerType, String alternateId) {
		return ((monomerDB.get(polymerType) != null) && getMonomer(polymerType,
				alternateId) != null);
	}

	/**
	 * Returns the monomer specified by polymerType and alternatId
	 * 
	 * @param polymerType
	 * @param alternateId
	 * @return the matching monomer
	 */
	public Monomer getMonomer(String polymerType, String alternateId) {
		return monomerDB.get(polymerType).get(alternateId);
	}

	/**
	 * Returns the monomer by smiles string
	 * 
	 * @param smiles
	 * @return the matching monomer
	 */
	public Monomer getMonomer(String smiles) {
		return smilesMonomerDB.get(smiles);
	}

	/**
	 * Returns all monomers by polymerType
	 * 
	 * @param polymerType
	 * @return All monomers with polymerType
	 */
	public Map<String, Monomer> getMonomers(String polymerType) {
		return monomerDB.get(polymerType);
	}

	/**
	 * Adds a monomer to the store and makes it a temporary new monomer
	 * 
	 * @param monomer
	 * @throws IOException
	 * @throws MonomerException
	 */
	public synchronized void addNewMonomer(Monomer monomer) throws IOException,
			MonomerException {
		monomer.setNewMonomer(true);
		addMonomer(monomer, true);
	}

	/**
	 * Checks for the empty store
	 * 
	 * @return true if the store is empty, false if not
	 */
	public boolean isMonomerStoreEmpty() {
		return (this.monomerDB == null || this.monomerDB.values() == null || this.monomerDB
				.values().size() == 0);
	}

	/**
	 * Clears the MonomerStore
	 */
	public synchronized void clearMonomers() {
		this.monomerDB.clear();
		this.smilesMonomerDB.clear();
	}

	public String toString() {
		String str = "";
		for (Map<String, Monomer> val : this.monomerDB.values()) {
			for (Monomer mon : val.values()) {
				str += mon.getAlternateId() + "(" + mon.getPolymerType()
						+ "); ";
			}
			str += System.getProperty("line.separator");
		}

		return str;
	}

	/**
	 * Returns the polymer type set
	 * 
	 * @return the polymer type set as Set<String>
	 */
	public Set<String> getPolymerTypeSet() {
		return monomerDB.keySet();
	}

	/**
	 * This method returns all monomers of the store as list sorted by polymer
	 * type
	 * 
	 * @return all monomers of store as List<Monomer>
	 */
	public List<Monomer> getAllMonomersList() {
		List<Monomer> monomers = new ArrayList<Monomer>();
		for (String polymerType : getPolymerTypeSet()) {
			Map<String, Monomer> map = getMonomers(polymerType);
			monomers.addAll(map.values());
		}
		return monomers;

	}
}
 public interface NotationConstant {
	public static final String NOTATION_SOURCE = "HELM Notation";
	public static final String NOTATION_DIRECTORY = System
			.getProperty("user.home")
			+ System.getProperty("file.separator")
			+ ".helm";

	// notation to smiles conversion will not be performed when total monomer
	// count is above this threshold
	public static final int MONOMER_COUNT_THRESHOLD = 200;

	// break long sequences to small chunks using the value
	public static final int MONOMER_GROUP_COUNT_INTERVAL = 20;

} public class NotationException extends Exception {

	/**
	 * Creates a new instance of <code>NotationException</code> without detail
	 * message.
	 */
	public NotationException() {
	}

	/**
	 * Constructs an instance of <code>NotationException</code> with the
	 * specified detail message.
	 * 
	 * @param msg
	 *            the detail message.
	 */
	public NotationException(String msg) {
		super(msg);
	}

	public NotationException(String msg, Throwable err) {
		super(msg, err);
	}
} public class NucleotideFactory {

	public static final String NOTATION_DIRECTORY = NotationConstant.NOTATION_DIRECTORY;
	public static final String LOCAL_NUCLEOTIDE_TEMPLATE_FILE_NAME = "NucleotideTemplates.xml";
	public static final String LOCAL_NUCLEOTIDE_TEMPLATE_FILE_PATH = NOTATION_DIRECTORY
			+ System.getProperty("file.separator")
			+ LOCAL_NUCLEOTIDE_TEMPLATE_FILE_NAME;
	public static final String NUCLEOTIDE_TEMPLATE_XML_RESOURCE = "resources/NucleotideTemplates.xml";
	public static final String NUCLEOTIDE_TEMPLATE_SCHEMA_RESOURCE = "resources/NucleotideTemplateSchema.xsd";
	public static final String XML_SCHEMA_VALIDATION_FEATURE = "http://apache.org/xml/features/validation/schema";
	public static final String EXTERNAL_SCHEMA_LOCATION_KEY = "http://apache.org/xml/properties/schema/external-schemaLocation";
	public static final String DEFAULT_NAME_SPACE = "lmr";
	private static NucleotideFactory instance;
	/**
	 * First key is notation source, such as "HELM Notation" Second key is
	 * nucleotide symbol, such as "A"
	 */
	private static Map<String, Map<String, String>> nucleotideTemplates;
	private static Map<String, String> reverseNucleotideMap;
	private static SAXBuilder builder;
	private static Logger logger = Logger.getLogger(NucleotideFactory.class
			.toString());

	public synchronized Map<String, Map<String, String>> getNucleotideTemplates() {
		return nucleotideTemplates;
	}

	public synchronized void setNucleotideTemplates(
			Map<String, Map<String, String>> newNucleotideTemplates) {
		nucleotideTemplates = newNucleotideTemplates;
		reverseNucleotideMap = getReverseNucleotideTemplateMap(NotationConstant.NOTATION_SOURCE);
	}

	private static void setupBuilder() {
		URL schema = MonomerFactory.class
				.getResource(NUCLEOTIDE_TEMPLATE_SCHEMA_RESOURCE);
		builder = new SAXBuilder(true); // checks both well-formedness and
										// validity
		builder.setFeature(XML_SCHEMA_VALIDATION_FEATURE, true);
		builder.setProperty(EXTERNAL_SCHEMA_LOCATION_KEY, DEFAULT_NAME_SPACE
				+ " " + schema.toString());
	}

	private NucleotideFactory() {
	}

	public static NucleotideFactory getInstance() throws IOException,
			JDOMException, NotationException {
		if (null == instance) {
			initializeNucleotideTemplates();
			instance = new NucleotideFactory();
		}
		return instance;
	}

	private static Map<String, Map<String, String>> buildNucleotideTemplates(
			InputStream templatesInputStream) throws IOException, JDOMException {
		if (null == builder) {
			setupBuilder();
		}
		Document doc = builder.build(templatesInputStream);
		Element root = doc.getRootElement();
		Map<String, Map<String, String>> templates = NucleotideSequenceParser
				.getNucleotideTemplates(root);
		return templates;
	}

	public Map<String, Map<String, String>> buildNucleotideTemplatesFromXML(
			String nucleotideTemplatesXML) throws IOException, JDOMException {
		ByteArrayInputStream bais = new ByteArrayInputStream(
				nucleotideTemplatesXML.getBytes());
		return buildNucleotideTemplates(bais);
	}

	/**
	 * This method is called during startup, use local version if exist,
	 * otherwise use XML version in jar
	 * 
	 * @throws org.helm.notation.MonomerException
	 * @throws java.io.IOException
	 * @throws org.jdom.JDOMException
	 */
	private static void initializeNucleotideTemplates() throws IOException,
			JDOMException, NotationException {

		InputStream in = null;
		File localFile = new File(LOCAL_NUCLEOTIDE_TEMPLATE_FILE_PATH);
		Map<String, Map<String, String>> templates = null;

		if (localFile.exists()) {
			try {
				in = new FileInputStream(localFile);
				templates = buildNucleotideTemplates(in);
				validate(templates);
				logger.log(Level.INFO, LOCAL_NUCLEOTIDE_TEMPLATE_FILE_PATH
						+ " is used for nucleotide templates initialization");
			} catch (Exception e) {
				logger.log(Level.INFO,
						"Unable to use local nucleotide templates for initialization");
				localFile.delete();
				logger.log(Level.INFO,
						"Deleted local nucleotide templates file");
			}
		}

		if (null == templates) {
			in = NucleotideFactory.class
					.getResourceAsStream(NUCLEOTIDE_TEMPLATE_XML_RESOURCE);
			templates = buildNucleotideTemplates(in);
			validate(templates);
			logger.log(Level.INFO, NUCLEOTIDE_TEMPLATE_XML_RESOURCE
					+ " is used for nucleotide templates initialization");
		}

		nucleotideTemplates = templates;
		reverseNucleotideMap = getReverseNucleotideTemplateMap(NotationConstant.NOTATION_SOURCE);
	}

	/**
	 * save Nucleotide Templates to disk file
	 * 
	 * @throws java.io.IOException
	 */
	public void saveNucleotideTemplates() throws IOException {
		File f = new File(NOTATION_DIRECTORY);
		if (!f.exists()) {
			f.mkdir();
		}
		String nucleotideTemplatesXML = NucleotideSequenceParser
				.getNucleotideTemplatesXML(getNucleotideTemplates());
		FileOutputStream fos = new FileOutputStream(
				LOCAL_NUCLEOTIDE_TEMPLATE_FILE_PATH);
		fos.write(nucleotideTemplatesXML.getBytes());
	}

	/**
	 * key is notation such as R(A)P, value is symblo such as A
	 * 
	 * @param notationSource
	 * @return notation/nucleotidSymbol map
	 */
	private static synchronized Map<String, String> getReverseNucleotideTemplateMap(
			String notationSource) {
		Map<String, String> map = new HashMap<String, String>();
		Map<String, String> normalMap = nucleotideTemplates.get(notationSource);
		if (normalMap != null) {
			Set<String> set = normalMap.keySet();
			for (Iterator i = set.iterator(); i.hasNext();) {
				String tmp = (String) i.next();
				map.put((String) normalMap.get(tmp), tmp);
			}
		}
		return map;
	}

	/**
	 * 
	 * @return reverse nucleotide template map for 'HELM Notation'
	 */
	public synchronized Map<String, String> getReverseNucleotideTemplateMap() {
		return reverseNucleotideMap;
	}

	private static boolean validate(Map<String, Map<String, String>> templates)
			throws IOException, NotationException, JDOMException {
		Map<String, String> nucMap = templates
				.get(NotationConstant.NOTATION_SOURCE);
		Set<String> symbols = nucMap.keySet();
		for (String symbol : symbols) {
			String notation = nucMap.get(symbol);
			try {
				SimpleNotationParser.validateSimpleNotationForRNA(notation);
			} catch (Exception e) {
				throw new NotationException("Invalid notation " + notation
						+ " for nucleotide " + symbol);
			}
		}
		return true;
	}
} public class StructureException extends Exception {

	/**
	 * Creates a new instance of <code>StructureException</code> without detail
	 * message.
	 */
	public StructureException() {
	}

	/**
	 * Constructs an instance of <code>StructureException</code> with the
	 * specified detail message.
	 * 
	 * @param msg
	 *            the detail message.
	 */
	public StructureException(String msg) {
		super(msg);
	}

	public StructureException(String msg, Throwable err) {
		super(msg, err);
	}
} public class Attachment implements Serializable {

	public static final String BACKBONE_MONOMER_LEFT_ATTACHEMENT = "R1";
	public static final String BACKBONE_MONOMER_RIGHT_ATTACHEMENT = "R2";
	public static final String BACKBONE_MONOMER_BRANCH_ATTACHEMENT = "R3";
	public static final String BRANCH_MONOMER_ATTACHEMENT = "R1";
	public static final String STARTING_MONOMER_ATTACHEMENT = "Starting";

	public static final String CAP_GROUP_OH = "OH";
	public static final String CAP_GROUP_H = "H";

	public static final String PAIR_ATTACHMENT = "pair";

	// surrogate database ID, users may never see it
	private int id;

	// String ID for this attachment
	private String alternateId;

	// R group used for this attachment, such as R1
	private String label;

	// The name for the capping group, such as hydroxyl or OH, does not specify
	// connection atom for multi-atom groups.
	private String capGroupName;

	// The canonical SMILES for the capping group, include the R group to
	// indicate how connection should be made with monomer
	private String capGroupSMILES;

	// private boolean connected;

	public Attachment() {
		// connected = false;
	}

	public Attachment(String label, String capGroupName) {
		this.label = label;
		this.capGroupName = capGroupName;
		// connected = false;
	}

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getAlternateId() {
		return alternateId;
	}

	public void setAlternateId(String alternateId) {
		this.alternateId = alternateId;
	}

	public String getLabel() {
		if (label == null) {
			return " ";
		}
		return label;
	}

	public void setLabel(String label) {
		this.label = label;
	}

	public String getCapGroupName() {
		return capGroupName;
	}

	public void setCapGroupName(String capGroupName) {
		this.capGroupName = capGroupName;
	}

	public String getCapGroupSMILES() {
		return capGroupSMILES;
	}

	public void setCapGroupSMILES(String capGroupSMILES) {
		this.capGroupSMILES = capGroupSMILES;
	}

	// public boolean isConnected() {
	// return connected;
	// }
	//
	// public void setConnected(boolean connected) {
	// this.connected = connected;
	// }

	@Override
	public String toString() {
		return (label + ": " + capGroupName);
	}
} public class MoleculeInfo {
	private double molecularWeight;
	private String molecularFormula;
	private double exactMass;

	public double getMolecularWeight() {
		return molecularWeight;
	}

	public void setMolecularWeight(double molecularWeight) {
		this.molecularWeight = molecularWeight;
	}

	public String getMolecularFormula() {
		return molecularFormula;
	}

	public void setMolecularFormula(String molecularFormula) {
		this.molecularFormula = molecularFormula;
	}

	public double getExactMass() {
		return exactMass;
	}

	public void setExactMass(double exactMass) {
		this.exactMass = exactMass;
	}
} public class Monomer implements Serializable {

	public static final String NUCLIEC_ACID_POLYMER_TYPE = "RNA";
	public static final String PEPTIDE_POLYMER_TYPE = "PEPTIDE";
	public static final String CHEMICAL_POLYMER_TYPE = "CHEM";
	public static final String[] SUPPORTED_POLYMER_TYPES = {
			NUCLIEC_ACID_POLYMER_TYPE, PEPTIDE_POLYMER_TYPE,
			CHEMICAL_POLYMER_TYPE };
	public static final String BACKBONE_MOMONER_TYPE = "Backbone";
	public static final String BRANCH_MOMONER_TYPE = "Branch";
	public static final String UNDEFINED_MOMONER_TYPE = "Undefined";
	public static final String STARTING_NAME = "5";
	public static final String ATTACHMENT_LIST_DELIMITER = "$"; // surrogate
																// database ID,
																// users may
																// never see it
	private int id; // unique text ID for the monomer, will be used in polymer
					// notation
	private String alternateId; // the ID of its closest natural analog
	private String naturalAnalog; // the long name of the monomer
	private String name; // canonical SMILES that represents the monomer
	private String canSMILES;
	private String molfile; // monomer type, Backone, Branch, UnDefined
	private String monomerType; // polymer type, NucleicAcid, Peptide,
								// ChemicalStructure
	private String polymerType; // list of attachments in the monomer
	private List<Attachment> attachmentList; // mark monomer as new
	private boolean newMonomer;
	private boolean adHocMonomer;
	public static final String ID_A = "A";
	public static final String ID_G = "G";
	public static final String ID_C = "C";
	public static final String ID_U = "U";
	public static final String ID_T = "T";
	public static final String ID_R = "R";
	public static final String ID_dR = "dR";
	public static final String ID_P = "P";
	public static final String ID_X = "X";
	public static final String ID_ALA = "Ala";
	public static final String ID_ARG = "Arg";
	public static final String ID_ASN = "Asn";
	public static final String ID_ASP = "Asp";
	public static final String ID_CYS = "Cys";
	public static final String ID_GLU = "Glu";
	public static final String ID_GLN = "Gln";
	public static final String ID_GLY = "Gly";
	public static final String ID_HIS = "His";
	public static final String ID_ILE = "Ile";
	public static final String ID_LEU = "Leu";
	public static final String ID_LYS = "Lys";
	public static final String ID_MET = "Met";
	public static final String ID_PHE = "Phe";
	public static final String ID_PRO = "Pro";
	public static final String ID_SER = "Ser";
	public static final String ID_THR = "Thr";
	public static final String ID_TRP = "Trp";
	public static final String ID_TYR = "Tyr";
	public static final String ID_VAL = "Val";
	public static final String ID_CHEMICAL_STRUCTURE = "chemical structure";

	/**
	 * constructor
	 */
	public Monomer() {
		attachmentList = new ArrayList<Attachment>();
		name = "";
	}

	/**
	 * Create a new monomer.
	 * 
	 * @param polymerType
	 *            : unique text ID for the monomer, will be used in polymer
	 *            notation
	 * @param monomerType
	 *            : monomer type, Backbone, Branch, UnDefined
	 * @param naturalAnalog
	 *            : the ID of its closest natural analog
	 * @param alternateId
	 *            : unique text ID for the monomer, will be used in polymer
	 *            notation
	 */
	public Monomer(String polymerType, String monomerType,
			String naturalAnalog, String alternateId) {
		attachmentList = new ArrayList<Attachment>();
		name = "";
		setPolymerType(polymerType);
		setMonomerType(monomerType);
		setNaturalAnalog(naturalAnalog);
		setAlternateId(alternateId);
	}

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getAlternateId() {
		return alternateId;
	}

	public void setAlternateId(String alternateId) {
		this.alternateId = alternateId;
	}

	public String getNaturalAnalog() {
            return naturalAnalog;
	}

	public void setNaturalAnalog(String naturalAnalog) {
		this.naturalAnalog = naturalAnalog;
	}

	public void setAdHocMonomer(boolean adHocMonomer) {
		this.adHocMonomer = adHocMonomer;
	}

	public boolean isAdHocMonomer() {
		return this.adHocMonomer;
	}

	public String getCanSMILES() {
		return canSMILES;
	}

	public void setCanSMILES(String canSMILES) {
		this.canSMILES = canSMILES;
	}

	public String getMonomerType() {
		return monomerType;
	}

	public void setMonomerType(String monomerType) {
		this.monomerType = monomerType;
	}

	public String getPolymerType() {
		return polymerType;
	}

	public void setPolymerType(String polymerType) {
		this.polymerType = polymerType;
	}

	public List<Attachment> getAttachmentList() {
		return attachmentList;
	}

	public void setAttachmentList(List<Attachment> attachmentList) {
		this.attachmentList = attachmentList;
	}

	public String getMolfile() {
		return molfile;
	}

	public void setMolfile(String molfile) {
		this.molfile = molfile;
	}

	/**
	 * get a specific attachment by passing in a label
	 * 
	 * @param label
	 *            : unique for each attach point
	 * @return Attachment or null if there is no such attach point
	 */
	public Attachment getAttachment(String label) {
		for (Attachment attachment : attachmentList) {
			if (attachment.getLabel().equalsIgnoreCase(label)) {
				return attachment;
			}
		}
		return null;
	}

	/**
	 * This method returns the MoleculeInfo for the input R group label of this
	 * monomer
	 * 
	 * @param label
	 *            - R1, R2...
	 * @return MoleculeInfo for the cap group, R group will contribute nothing
	 * @throws IOException
	 * @throws PluginException
	 */
	public MoleculeInfo getCapMoleculeInfo(String label) throws IOException,
			PluginException {
		for (Attachment attachment : attachmentList) {
			if (attachment.getLabel().equalsIgnoreCase(label)) {
				String capSmi = attachment.getCapGroupSMILES();
				return StructureParser.getMoleculeInfo(capSmi);
			}
		}
		return null;
	}

	/**
	 * Try to add a new attachment to this monomer
	 * 
	 * @param attachment
	 *            -- new attachment to be add in
	 * @return true for success and false if there is one such attach point
	 *         exist
	 */
	public boolean addAttachment(Attachment attachment) {
		boolean isExist = false;
		for (Attachment a : attachmentList) {
			if (a.getLabel().equalsIgnoreCase(attachment.getLabel())) {
				isExist = true;
			}
		}
		if (!isExist) {
			return attachmentList.add(attachment);
		}
		return false;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	/**
	 * Test if this monomer has been modified. The length of the alternatedId of
	 * an modified monomer should be greater than one.
	 * 
	 * @return true/false
	 */
	public boolean isModified() {
		return (alternateId.length() > 1);
	}

	/**
	 * Compare this momoner with another one, polymerType, monomerType and
	 * naturalAnalog (could be derived) must be the same to return true
	 * 
	 * @param m
	 * @return true or false
	 */
	public boolean isSameType(Monomer m) {
		String analog1 = getNaturalAnalog();
		if (null == analog1) {
			analog1 = getAlternateId();
		}
		String analog2 = m.getNaturalAnalog();
		if (null == analog2) {
			analog2 = m.getAlternateId();
		}
		if (getMonomerType().equals(m.getMonomerType())
				&& getPolymerType().equals(m.getPolymerType())
				&& analog1.equals(analog2)) {
			return true;
		} else {
			return false;
		}
	}

	public String getAttachmentListString() {
		StringBuilder sb = new StringBuilder();
		List<Attachment> al = this.getAttachmentList();
		List<String> l = new ArrayList<String>();
		for (Attachment a : al) {
			l.add(a.getAlternateId());
		}
		Collections.sort(l);

		for (int i = 0; i < l.size(); i++) {
			if (sb.length() > 0) {
				sb.append(ATTACHMENT_LIST_DELIMITER);
			}
			sb.append(l.get(i));
		}
		return sb.toString();
	}

	public boolean isNewMonomer() {
		return newMonomer;
	}

	public void setNewMonomer(boolean newMonomer) {
		this.newMonomer = newMonomer;
	}

	public boolean attachmentEquals(Monomer monomer) {
		String tmpListString = this.getAttachmentListString();
		String monomerListString = monomer.getAttachmentListString();
		return tmpListString.equals(monomerListString);
	}

	public boolean attachmentContains(Monomer monomer) {
		String tmpListString = this.getAttachmentListString();
		String monomerListString = monomer.getAttachmentListString();
		int index = tmpListString.indexOf(monomerListString);
		if (index >= 0) {
			return true;
		} else {
			return false;
		}
	}

	public boolean containAnyAtom() throws IOException {
		boolean containsA = false;
		String smiles = getCanSMILES();
		if (null != smiles && smiles.length() > 0) {
			Molecule mol = StructureParser.getMolecule(smiles);
			MolAtom[] atoms = mol.getAtomArray();
			for (MolAtom atom : atoms) {
				String symbol = atom.getSymbol();
				if ("A".equals(symbol)) {
					containsA = true;
					break;
				}
			}
		}
		return containsA;
	}
} public class Nucleotide implements Serializable {

	public static final int STARTING_POSITION_TYPE = 1;
	public static final int MIDDLE_POSITION_TYPE = 2;
	public static final int ENDING_POSITION_TYPE = 3;

	private int positionType = MIDDLE_POSITION_TYPE;

	private String symbol;
	private boolean modified;
	private String notation;
	private String naturalAnalog;

	public Nucleotide() {
	}

	public Nucleotide(String symbol, String notation) {
		this.symbol = symbol;
		this.notation = notation;
	}

	public Nucleotide(String notation, int postionType) {
		this.notation = notation;
		this.positionType = postionType;
	}

	public Nucleotide(String symbol, String notation, int postionType) {
		this.symbol = symbol;
		this.notation = notation;
		this.positionType = postionType;
	}

	public int getPositionType() {
		return positionType;
	}

	public void setPositionType(int positionType) {
		this.positionType = positionType;
	}

	public String getSymbol() {
		return symbol;
	}

	public void setSymbol(String symbol) {
		this.symbol = symbol;
	}

	public boolean isModified() {
		if (null != getNotation()) {

			// has modifier
			if (getNotation().contains("["))
				return true;

			// has X as base
			if (getNotation().contains("(X)"))
				return true;

			// ends with ")", i.e.,no phophate
			if (getNotation().endsWith(")"))
				return true;
		}

		return false;
	}

	/**
	 * Returns true if the nucleotide is unmodified except for a missing
	 * phosphate group. If the nucleotide notation is empty, it returns true.
	 * 
	 * @return true or false
	 */
	public boolean unmodifiedWithoutPhosphate() {
		if (getNotation() == null) {
			return true;
		} else {
			// System.out.println("Contains [ " + getNotation().contains("[") +
			// "\tContainsX " + getNotation().contains("["));
			if (getNotation().contains("[") || getNotation().contains("X")) {
				return false;
			} else {
				return true;
			}
		}
	}

	@Deprecated
	public void setModified(boolean modified) {
		this.modified = modified;
	}

	public String getNotation() {
		return notation;
	}

	public void setNotation(String notation) {
		this.notation = notation;
	}

	/**
	 * return the natural analog of this nucleotide
	 * 
	 * @return natural Analog or X if natural analog is not available
	 */

	public String getNaturalAnalog() {
		MonomerFactory factory = null;
		try {
			factory = MonomerFactory.getInstance();
		} catch (Exception ex) {
			Logger.getLogger(Nucleotide.class.getName()).log(Level.SEVERE,
					"Unable to initialize monomer factory", ex);
		}
		return getNaturalAnalog(factory.getMonomerStore());
	}

	/**
	 * return the natural analog of this nucleotide
	 * 
	 * @param monomerStore
	 *            - store in which base monomer is located
	 * @return natural Analog or X if natural analog is not available
	 */

	public String getNaturalAnalog(MonomerStore monomerStore) {
		String baseNotation = null;
		String notation = getNotation();
		char[] notationChar = notation.toCharArray();
		for (int i = 0; i < notationChar.length; i++) {
			if (notationChar[i] == '[') {
				int pos = SimpleNotationParser.getMatchingBracketPosition(
						notationChar, i, '[', ']');
				i = pos;
				continue;
			}
			// must be the base
			if (notationChar[i] == '(') {
				int pos = SimpleNotationParser.getMatchingBracketPosition(
						notationChar, i, '(', ')');
				baseNotation = notation.substring(i + 1, pos);
				break;

			}
		}

		// No base found
		if (baseNotation == null) {
			return "X";
		}
		// remove first and last bracket
		if ((baseNotation.charAt(0) == '[')
				&& (baseNotation.charAt(baseNotation.length() - 1) == ']')) {
			baseNotation = baseNotation.substring(1, baseNotation.length() - 1);
		} else {
			baseNotation = baseNotation;
		}

		try {

			Map<String, Monomer> monomers = monomerStore
					.getMonomers(Monomer.NUCLIEC_ACID_POLYMER_TYPE);
			Monomer m = monomers.get(baseNotation);

			if (m == null) {
				Map<String, Monomer> smiles = monomerStore.getSmilesMonomerDB();
				m = smiles.get(baseNotation);
			}

			return m.getNaturalAnalog();
		} catch (Exception e) {
			System.out.println("Unable to get natural analog for "
					+ baseNotation);
			return "X";
		}
	}

	@Deprecated
	public void setNaturalAnalog(String naturalAnalog) {
		this.naturalAnalog = naturalAnalog;
	}

	/**
	 * return the phosphate monomer of this nucleotide
	 * 
	 * @return phosphate monomer
	 */
	public Monomer getPhosphateMonomer() {
		MonomerFactory factory = null;
		try {
			factory = MonomerFactory.getInstance();
		} catch (Exception ex) {
			Logger.getLogger(Nucleotide.class.getName()).log(Level.SEVERE,
					"Unable to initialize monomer factory", ex);
		}
		return getPhosphateMonomer(factory.getMonomerStore());
	}

	/**
	 * return the phosphate monomer of this nucleotide
	 * 
	 * @param monomerStore
	 *            - store in which phosphate monomer is located
	 * @return phosphate monomer
	 */
	public Monomer getPhosphateMonomer(MonomerStore monomerStore) {
		String phosphateSymbol = getPhosphateSymbol();
		if (phosphateSymbol != null && !phosphateSymbol.equalsIgnoreCase("")) {
			try {
				Map<String, Monomer> monomers = monomerStore
						.getMonomers(Monomer.NUCLIEC_ACID_POLYMER_TYPE);
				Monomer m = monomers.get(phosphateSymbol);
				return m;
			} catch (Exception ex) {
				System.out.println("Unable to get phosphate monomer for "
						+ phosphateSymbol);
				return null;
			}
		} else {
			return null;
		}
	}

	/**
	 * get the base monomer, the return value could be null if this nucleotide
	 * does not have a base
	 * 
	 * @return base monomer, could be null
	 */
	public Monomer getBaseMonomer() {
		MonomerFactory factory = null;
		try {
			factory = MonomerFactory.getInstance();
		} catch (Exception ex) {
			Logger.getLogger(Nucleotide.class.getName()).log(Level.SEVERE,
					"Unable to initialize monomer factory", ex);
		}
		return getBaseMonomer(factory.getMonomerStore());

	}

	/**
	 * get the base monomer, the return value could be null if this nucleotide
	 * does not have a base
	 * 
	 * @param monomerStore
	 *            - store in which base monomer is located
	 * @return base monomer, could be null
	 */
	public Monomer getBaseMonomer(MonomerStore monomerStore) {

		String baseSymbol = getBaseSymbol();
		if (baseSymbol != null && !baseSymbol.equalsIgnoreCase("")) {
			try {
				Map<String, Monomer> monomers = monomerStore
						.getMonomers(Monomer.NUCLIEC_ACID_POLYMER_TYPE);
				Monomer m = monomers.get(baseSymbol);
				return m;
			} catch (Exception ex) {
				System.out.println("Unable to get base monomer for "
						+ baseSymbol);
				return null;
			}
		} else {
			return null;
		}

	}

	/**
	 * get the sugar monomer, the return value could be null if the "nucleotide"
	 * does not has a sugar
	 * 
	 * @return sugar monomer
	 */
	public Monomer getSugarMonomer() {
		MonomerFactory factory = null;
		try {
			factory = MonomerFactory.getInstance();
		} catch (Exception ex) {
			Logger.getLogger(Nucleotide.class.getName()).log(Level.SEVERE,
					"Unable to initialize monomer factory", ex);
		}
		return getSugarMonomer(factory.getMonomerStore());
	}

	/**
	 * get the sugar monomer, the return value could be null if the "nucleotide"
	 * does not has a sugar
	 * 
	 * @param monomerStore
	 *            - store in which sugar monomer is located
	 * @return sugar monomer
	 */
	public Monomer getSugarMonomer(MonomerStore monomerStore) {
		String sugarSymbol = getSugarSymbol();
		if (sugarSymbol != null && !sugarSymbol.equalsIgnoreCase("")) {
			try {
				Map<String, Monomer> monomers = monomerStore
						.getMonomers(Monomer.NUCLIEC_ACID_POLYMER_TYPE);
				Monomer m = monomers.get(sugarSymbol);
				return m;
			} catch (Exception ex) {
				System.out.println("Unable to get sugar monomer for "
						+ sugarSymbol);
				return null;
			}
		} else {
			return null;
		}
	}

	/**
	 * This method returns the HELM notation for nucleotide linker
	 * 
	 * @return linker notation
	 */
	public String getLinkerNotation() {
		String pSymbol = getPhosphateSymbol();
		String result = null;
		if (null == pSymbol || pSymbol.length() == 0) {
			result = "";
		} else {
			if (pSymbol.length() > 1)
				result = "[" + pSymbol + "]";
			else
				result = pSymbol;
		}
		return result;
	}

	/**
	 * This method returns the HELM notation for nucleoside
	 * 
	 * @return nucleodie notation
	 */
	public String getNucleosideNotation() {
		int linkerLen = getLinkerNotation().length();
		return notation.substring(0, notation.length() - linkerLen);
	}

	/**
	 * find the phosphate symbol, if it is a modified monomer, the "[" and "]"
	 * will be removed
	 * 
	 * @return linker monomer ID
	 */
	private String getPhosphateSymbol() {
		String phosphateSymbol = null;
		// nucleotide notation
		String ncNotation = getNotation();
		// nucleotide has base
		if (ncNotation.contains("(")) {
			int start = ncNotation.indexOf(")");
			phosphateSymbol = ncNotation.substring(start + 1);
			phosphateSymbol = phosphateSymbol.replaceAll("\\[|\\]", "");
		} else {
			if (getPositionType() == ENDING_POSITION_TYPE) {
				if (ncNotation.startsWith("[")) {
					phosphateSymbol = ncNotation.substring(ncNotation
							.indexOf("]") + 1);
					phosphateSymbol = phosphateSymbol.replaceAll("\\[|\\]", "");
				} else {
					phosphateSymbol = ncNotation.substring(1);
				}
			} else {
				if (ncNotation.endsWith("]")) {
					char[] chars = ncNotation.toCharArray();
					for (int i = chars.length; i > 0; i--) {
						String letter = String.valueOf(chars[i - 1]);
						if (letter.equals("[")) {
							phosphateSymbol = ncNotation.substring(i);
							phosphateSymbol = phosphateSymbol.replaceAll(
									"\\[|\\]", "");
							break;
						}
					}
				} else {
					int pos = ncNotation.length() - 1;
					phosphateSymbol = ncNotation.substring(pos);
				}
			}
		}
		return phosphateSymbol;
	}

	/**
	 * find the sugar symbol, if it is a modified monomer, the "[" and "]" will
	 * be removed
	 * 
	 * @return sugar monomer ID
	 */
	private String getSugarSymbol() {
		String sugarSymbol = null;
		// nucleotide notation
		String ncNotation = getNotation();
		// nucleotide has base
		if (ncNotation.contains("(")) {
			int start = ncNotation.indexOf("(");
			sugarSymbol = ncNotation.substring(0, start);
			sugarSymbol = sugarSymbol.replaceAll("\\[|\\]", "");
		} else {
			if (getPositionType() == STARTING_POSITION_TYPE) {
				if (ncNotation.endsWith("]")) {
					char[] chars = ncNotation.toCharArray();
					for (int i = chars.length; i > 0; i--) {
						String letter = String.valueOf(chars[i - 1]);
						if (letter.equals("[")) {
							sugarSymbol = ncNotation.substring(0, i);
							sugarSymbol = sugarSymbol.replaceAll("\\[|\\]", "");
							break;
						}
					}
				} else {
					sugarSymbol = ncNotation.substring(0,
							ncNotation.length() - 1);
					sugarSymbol = sugarSymbol.replaceAll("\\[|\\]", "");
				}
			} else {
				if (ncNotation.startsWith("[")) {
					sugarSymbol = ncNotation.substring(0,
							ncNotation.indexOf("]"));
					sugarSymbol = sugarSymbol.replaceAll("\\[|\\]", "");
				} else {
					sugarSymbol = ncNotation.substring(0, 1);
				}
			}
		}
		return sugarSymbol;
	}

	/**
	 * find the base symbol, if it is a modified monomer, the "[" and "]" will
	 * be removed
	 * 
	 * @return base monomer ID
	 */
	private String getBaseSymbol() {
		String ncNotation = getNotation();
		if (!ncNotation.contains("(")) {
			return null;
		}
		int start = ncNotation.indexOf("(");
		int end = ncNotation.indexOf(")");

		String baseSymbol = ncNotation.substring(start + 1, end);
		baseSymbol = baseSymbol.replaceAll("\\[|\\]", "");
		return baseSymbol;
	}

} public class RgroupStructure {
	private Molecule molecule;
	private Map<String, MolAtom> rgroupMap;

	public Molecule getMolecule() {
		return molecule;
	}

	public void setMolecule(Molecule molecule) {
		this.molecule = molecule;
	}

	public Map<String, MolAtom> getRgroupMap() {
		return rgroupMap;
	}

	public void setRgroupMap(Map<String, MolAtom> rgroupMap) {
		this.rgroupMap = rgroupMap;
	}

}
import chemaxon.marvin.plugin.PluginException;
import chemaxon.struc.MolAtom;
import chemaxon.struc.Molecule;
import org.helm.notation.MonomerException;
import org.helm.notation.MonomerFactory;
import org.helm.notation.MonomerStore;
import org.helm.notation.NotationConstant;
import org.helm.notation.NotationException;
import org.helm.notation.NucleotideFactory;
import org.helm.notation.StructureException;
import org.helm.notation.model.Attachment;
import org.helm.notation.model.MoleculeInfo;
import org.helm.notation.model.Monomer;
import org.helm.notation.model.Nucleotide;
import org.helm.notation.model.RgroupStructure;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.jdom.JDOMException;

/*******************************************************************************
 * Copyright C 2012, The Pistoia Alliance
 * 
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 ******************************************************************************/
package org.helm.notation.tools;





/**
 * 2/24/2011 by Tianhong
 * extends to support ad hoc CHEM monomers, monomer could be added as extended ChemAxon SMILES, instead of ID
 * general process is to add it into monomer database factory first, then treat as normal monomer. The default attachment group will be Hydrogen, as R1-H, R2-H
 * 
 */

/**
 * This class provides methods that handle simple polymer notation. It is
 * unlikely that they will be called by clients directly.
 * 
 * @author zhangtianhong
 */
public class SimpleNotationParser {

	public static final String GROUP_LEVEL_DELIMITER_REGEX = "\\.";
	public static final String GROUP_LEVEL_DELIMITER = ".";
	public static final char MODIFICATION_START_SYMBOL = '[';
	public static final char MODIFICATION_END_SYMBOL = ']';

	public static final String MODIFICATION_DELIMITER_REGEX = "[\\[|\\]]";
	public static final char BRANCH_START_SYMBOL = '(';
	public static final char BRANCH_END_SYMBOL = ')';

	public static final String BRANCH_DELIMITER_REGEX = "[(|)]";
	public static final String RNA_SENSE_STRAND_ANNOTATION = "ss";
	public static final String RNA_ANTISENSE_STRAND_ANNOTATION = "as";

	/**
	 * This method generates the SMILES string for simple polymer notation
	 * 
	 * @param polymerNotation
	 * @param polymerType
	 * @return SMILES string for the simple polymer notation, still contains R
	 *         groups
	 * @throws java.io.IOException
	 * @throws org.helm.notation.NotationException
	 * @throws org.helm.notation.StructureException
	 * @throws org.helm.notation.MonomerException
	 */
	public static String getSimplePolymerSMILES(String polymerNotation,
			String polymerType) throws IOException, NotationException,
			StructureException, MonomerException, JDOMException {

		MonomerFactory factory = null;
