Skip to content
Snippets Groups Projects
Commit 1901a766 authored by brinn's avatar brinn
Browse files

add: class UniprotQuery

SVN: 13709
parent 41ad7b46
No related merge requests found
......@@ -21,5 +21,7 @@
<classpathentry kind="lib" path="/libraries/cisd-base/cisd-base.jar" sourcepath="/libraries/cisd-base/cisd-base-src.zip"/>
<classpathentry kind="lib" path="/libraries/fast-md5/fast-md5.jar" sourcepath="/libraries/fast-md5/src.zip"/>
<classpathentry kind="lib" path="/libraries/jython/jython.jar" sourcepath="/libraries/jython/src.zip"/>
<classpathentry kind="lib" path="/libraries/commons-httpclient/commons-httpclient.jar" sourcepath="/libraries/commons-httpclient/src.zip"/>
<classpathentry kind="lib" path="/libraries/commons-logging/commons-logging.jar" sourcepath="/libraries/commons-logging/src.zip"/>
<classpathentry kind="output" path="targets/classes"/>
</classpath>
/*
* Copyright 2009 ETH Zuerich, CISD
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.systemsx.cisd.common.net.uniprot;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* An enum of all available columns in the Uniprot database.
*
* @author Bernd Rinn
*/
public enum UniprotColumn
{
CITATION("citation", "PubMed ID"), COMMENTS("comments", "Comments"), DATABASE("database",
"Database"), DOMAINS("domains", "Domains"), DOMAIN("domain", "Domain"), EC("ec",
"EC numbers"), ID("id", "Accession"), ENTRY_NAME("entry%20name", "Entry name"),
EXISTENCE("existence", "Protein existence"), FAMILIES("families", "Protein family"), FEATURES(
"features", "Features"), GENES("genes", "Gene names"), GO("go", "Gene Ontology"),
GO_ID("go-id", "Gene Ontology ID"), INTERPRO("interpro", "InterPro"), INTERACTOR("interactor",
"Interacts with"), KEYWORDS("keywords", "Keywords"), LAST_MODIFIED("last-modified",
"Date of last modification"), LENGTH("length", "Length"), ORGANISM("organism",
"Organism"), ORGANISM_ID("organism-id", "Organism ID"), PATHWAY("pathway", "Pathway"),
PROTEIN_NAMES("protein%20names", "Protein names"), SCORE("score", "Score"), SEQUENCE(
"sequence", "Sequence"), STATUS("reviewed", "Status"), SUBCELLULAR_LOCATIONS(
"subcellular%20locations", "Subcellular locations"), TAXON("taxon", "Taxon"), THREED(
"3d", "3D"), VERSION("version", "Version"), VIRUS_HOSTS("virus%20hosts",
"Virus hosts\n");
/**
* The map from the column header in the Uniprot result set (in lower case characters) and the
* {@link UniprotColumn}.
*/
static final Map<String, UniprotColumn> columnMap;
static
{
final Map<String, UniprotColumn> myColumnMap = new HashMap<String, UniprotColumn>();
for (UniprotColumn col : values())
{
myColumnMap.put(col.getColumnHeader().toLowerCase(), col);
}
columnMap = Collections.unmodifiableMap(myColumnMap);
}
private String fieldName;
private String columnHeader;
UniprotColumn(String fieldName, String columnHeader)
{
this.fieldName = fieldName;
this.columnHeader = columnHeader;
}
public String getFieldName()
{
return fieldName;
}
public String getColumnHeader()
{
return columnHeader;
}
}
\ No newline at end of file
/*
* Copyright 2009 ETH Zuerich, CISD
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.systemsx.cisd.common.net.uniprot;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.apache.commons.lang.StringUtils;
import ch.systemsx.cisd.common.parser.ParserException;
import ch.systemsx.cisd.common.utilities.DateFormatThreadLocal;
/**
* A data transfer object for a Uniprot database entry.
*
* @author Bernd Rinn
*/
public class UniprotEntry
{
/** The Uniprot date format pattern. */
static final String UNIPROT_DATE_FORMAT_PATTERN = "yyyy-MM-dd";
static final ThreadLocal<SimpleDateFormat> DATE_FORMAT =
new DateFormatThreadLocal(UNIPROT_DATE_FORMAT_PATTERN);
private String citation;
private String comments;
private String database;
private String domains;
private String domain;
private String ec;
private String id;
private String entryName;
private String existence;
private String families;
private String features;
private String genes;
private String go;
private String goId;
private String interpro;
private String interactor;
private String keywords;
private Date lastModified;
private Integer length;
private String organism;
private String organismId;
private String pathway;
private String proteinNames;
private String status;
private String score;
private String sequence;
private String threeD;
private String subcellularLocations;
private String taxon;
private Integer version;
private String virusHosts;
public String getCitation()
{
return citation;
}
void setCitation(String citation)
{
this.citation = citation;
}
public String getComments()
{
return comments;
}
void setComments(String comments)
{
this.comments = comments;
}
public String getDatabase()
{
return database;
}
void setDatabase(String database)
{
this.database = database;
}
public String getDomains()
{
return domains;
}
void setDomains(String domains)
{
this.domains = domains;
}
public String getDomain()
{
return domain;
}
void setDomain(String domain)
{
this.domain = domain;
}
public String getEc()
{
return ec;
}
void setEc(String ec)
{
this.ec = ec;
}
public String getId()
{
return id;
}
void setId(String id)
{
this.id = id;
}
public String getEntryName()
{
return entryName;
}
void setEntryName(String entryName)
{
this.entryName = entryName;
}
public String getExistence()
{
return existence;
}
void setExistence(String existence)
{
this.existence = existence;
}
public String getFamilies()
{
return families;
}
void setFamilies(String families)
{
this.families = families;
}
public String getFeatures()
{
return features;
}
void setFeatures(String features)
{
this.features = features;
}
public String getGenes()
{
return genes;
}
void setGenes(String genes)
{
this.genes = genes;
}
public String getGo()
{
return go;
}
void setGo(String go)
{
this.go = go;
}
public String getGoId()
{
return goId;
}
void setGoId(String goId)
{
this.goId = goId;
}
public String getInterpro()
{
return interpro;
}
void setInterpro(String interpro)
{
this.interpro = interpro;
}
public String getInteractor()
{
return interactor;
}
void setInteractor(String interactor)
{
this.interactor = interactor;
}
public String getKeywords()
{
return keywords;
}
void setKeywords(String keywords)
{
this.keywords = keywords;
}
public Date getLastModified()
{
return lastModified;
}
public String getLastModifiedStr()
{
return DATE_FORMAT.get().format(lastModified);
}
void setLastModified(Date lastModified)
{
this.lastModified = lastModified;
}
public Integer getLength()
{
return length;
}
void setLength(Integer length)
{
this.length = length;
}
public String getOrganism()
{
return organism;
}
void setOrganism(String organism)
{
this.organism = organism;
}
public String getOrganismId()
{
return organismId;
}
void setOrganismId(String organismId)
{
this.organismId = organismId;
}
public String getPathway()
{
return pathway;
}
void setPathway(String pathway)
{
this.pathway = pathway;
}
public String getProteinNames()
{
return proteinNames;
}
void setProteinNames(String proteinNames)
{
this.proteinNames = proteinNames;
}
public String getStatus()
{
return status;
}
void setStatus(String status)
{
this.status = status;
}
public String getScore()
{
return score;
}
void setScore(String score)
{
this.score = score;
}
public String getSequence()
{
return sequence;
}
void setSequence(String sequence)
{
this.sequence = sequence;
}
public String getThreeD()
{
return threeD;
}
void setThreeD(String threeD)
{
this.threeD = threeD;
}
public String getSubcellularLocations()
{
return subcellularLocations;
}
void setSubcellularLocations(String subcellularLocations)
{
this.subcellularLocations = subcellularLocations;
}
public String getTaxon()
{
return taxon;
}
void setTaxon(String taxon)
{
this.taxon = taxon;
}
public Integer getVersion()
{
return version;
}
void setVersion(Integer version)
{
this.version = version;
}
public String getVirusHosts()
{
return virusHosts;
}
void setVirusHosts(String virusHosts)
{
this.virusHosts = virusHosts;
}
void set(UniprotColumn column, String value)
{
switch (column)
{
case CITATION:
setCitation(value);
break;
case COMMENTS:
setComments(value);
break;
case DATABASE:
setDatabase(value);
break;
case DOMAIN:
setDomain(value);
break;
case DOMAINS:
setDomains(value);
break;
case EC:
setEc(value);
break;
case ENTRY_NAME:
setEntryName(value);
break;
case EXISTENCE:
setExistence(value);
break;
case FAMILIES:
setFamilies(value);
break;
case FEATURES:
setFeatures(value);
break;
case GENES:
setGenes(value);
break;
case GO:
setGo(value);
break;
case GO_ID:
setGoId(value);
break;
case ID:
setId(value);
break;
case INTERACTOR:
setInteractor(value);
break;
case INTERPRO:
setInterpro(value);
break;
case KEYWORDS:
setKeywords(value);
break;
case LAST_MODIFIED:
if (StringUtils.isNotBlank(value))
{
try
{
setLastModified(DATE_FORMAT.get().parse(value));
} catch (ParseException ex)
{
throw new ParserException("Error parsing date: " + value, ex);
}
}
break;
case LENGTH:
if (StringUtils.isNotBlank(value))
{
setLength(Integer.parseInt(value));
}
break;
case ORGANISM:
setOrganism(value);
break;
case ORGANISM_ID:
setOrganismId(value);
break;
case PATHWAY:
setPathway(value);
break;
case PROTEIN_NAMES:
setProteinNames(value);
break;
case SCORE:
setScore(value);
break;
case SEQUENCE:
setSequence(value);
break;
case STATUS:
setStatus(value);
break;
case SUBCELLULAR_LOCATIONS:
setSubcellularLocations(value);
break;
case TAXON:
setTaxon(value);
break;
case THREED:
setThreeD(value);
break;
case VERSION:
if (StringUtils.isNotBlank(value))
{
setVersion(Integer.parseInt(value));
}
break;
case VIRUS_HOSTS:
setVirusHosts(value);
break;
}
}
}
/*
* Copyright 2009 ETH Zuerich, CISD
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.systemsx.cisd.common.net.uniprot;
import java.util.Set;
import ch.systemsx.cisd.common.parser.IParserObjectFactory;
import ch.systemsx.cisd.common.parser.IPropertyMapper;
import ch.systemsx.cisd.common.parser.IPropertyModel;
import ch.systemsx.cisd.common.parser.ParserException;
/**
* A parser factory for {@link UniprotEntry}s.
*
* @author Bernd Rinn
*/
final class UniprotEntryParserFactory implements IParserObjectFactory<UniprotEntry>
{
private final UniprotColumn[] columns;
UniprotEntryParserFactory(IPropertyMapper mapper)
{
final Set<String> columnHeaders = mapper.getAllPropertyCodes();
this.columns = new UniprotColumn[columnHeaders.size()];
for (String columnHeader : columnHeaders)
{
final IPropertyModel model = mapper.getPropertyModel(columnHeader);
final UniprotColumn col = UniprotColumn.columnMap.get(model.getCode().toLowerCase());
if (col == null)
{
throw new ParserException("Unknown Uniprot column header: '" + model.getCode()
+ "'");
}
columns[model.getColumn()] = col;
}
}
public UniprotEntry createObject(String[] lineTokens) throws ParserException
{
assert lineTokens.length == columns.length;
final UniprotEntry result = new UniprotEntry();
for (int i = 0; i < columns.length; ++i)
{
final UniprotColumn column = columns[i];
result.set(column, lineTokens[i]);
}
return result;
}
}
/*
* Copyright 2009 ETH Zuerich, CISD
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.systemsx.cisd.common.net.uniprot;
import static ch.systemsx.cisd.common.net.uniprot.UniprotColumn.ID;
import java.io.IOException;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;
import ch.systemsx.cisd.base.exceptions.CheckedExceptionTunnel;
import ch.systemsx.cisd.base.exceptions.IOExceptionUnchecked;
import ch.systemsx.cisd.common.parser.IParserObjectFactory;
import ch.systemsx.cisd.common.parser.IParserObjectFactoryFactory;
import ch.systemsx.cisd.common.parser.IPropertyMapper;
import ch.systemsx.cisd.common.parser.ParserException;
import ch.systemsx.cisd.common.parser.TabFileLoader;
/**
* A class for querying the Uniprot database.
*
* @author Bernd Rinn
*/
public final class UniprotQuery
{
private static final int RETRY_COUNT = 3;
private static final String BASE_URL = "http://www.uniprot.org/uniprot/";
private static final String QUERY_INIT_STR = "query=";
private static final String COLUMN_INIT_STR = "columns=";
private static final String FORMAT_STR = "format=tab";
private static final String OFFSET_STR = "offset=";
private static final String LIMIT_STR = "limit=";
/**
* Create a set of Uniprot columns.
*/
public static Set<UniprotColumn> columns(UniprotColumn... columns)
{
final Set<UniprotColumn> set = EnumSet.noneOf(UniprotColumn.class);
for (UniprotColumn col : columns)
{
set.add(col);
}
return set;
}
private final String columnsSpecification;
/**
* Construct a Uniprot query, adding the given database <var>columns</var>. Note that
* {@link UniprotColumn#ID} will always be added to the set of columns.
*/
public UniprotQuery(UniprotColumn... columns)
{
this(columns(columns));
}
/**
* Construct a Uniprot query, adding the given database <var>columns</var>. Note that
* {@link UniprotColumn#ID} will always be added to the set of columns.
*/
public UniprotQuery(Set<UniprotColumn> columns)
{
columns.add(ID);
columnsSpecification = createColumnSpecification(columns);
}
private String createColumnSpecification(Set<UniprotColumn> columns)
{
final StringBuilder builder = new StringBuilder();
builder.append(COLUMN_INIT_STR);
for (UniprotColumn col : columns)
{
builder.append(col.getFieldName());
builder.append(',');
}
builder.setLength(builder.length() - 1);
return builder.toString();
}
private String buildQueryURLForKeys(List<String> keys)
{
final StringBuilder builder = new StringBuilder();
builder.append(BASE_URL);
builder.append('?');
builder.append(QUERY_INIT_STR);
for (String key : keys)
{
builder.append("accession:");
builder.append(key);
builder.append("+or+");
}
builder.setLength(builder.length() - "+or+".length());
builder.append('&');
builder.append(FORMAT_STR);
builder.append('&');
builder.append(columnsSpecification);
return builder.toString();
}
private String buildQueryURLForQueryExpression(String queryExpression, int limit, int offset)
{
final StringBuilder builder = new StringBuilder();
builder.append(BASE_URL);
builder.append('?');
builder.append(QUERY_INIT_STR);
builder.append(queryExpression.replaceAll(" ", "%20"));
builder.append('&');
builder.append(FORMAT_STR);
builder.append('&');
builder.append(columnsSpecification);
if (limit > 0)
{
builder.append('&');
builder.append(LIMIT_STR);
builder.append(Integer.toString(limit));
}
if (offset > 0)
{
builder.append('&');
builder.append(OFFSET_STR);
builder.append(Integer.toString(offset));
}
return builder.toString();
}
private Iterable<UniprotEntry> runQuery(final String queryURL) throws IOExceptionUnchecked
{
final HttpClient client = new HttpClient();
final GetMethod method = new GetMethod(queryURL);
method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
new DefaultHttpMethodRetryHandler(RETRY_COUNT, false));
try
{
final int statusCode = client.executeMethod(method);
if (statusCode != HttpStatus.SC_OK)
{
throw new IOExceptionUnchecked(new IOException("GET failed: "
+ method.getStatusLine()));
}
final TabFileLoader<UniprotEntry> parser =
new TabFileLoader<UniprotEntry>(new IParserObjectFactoryFactory<UniprotEntry>()
{
public IParserObjectFactory<UniprotEntry> createFactory(
IPropertyMapper propertyMapper) throws ParserException
{
return new UniprotEntryParserFactory(propertyMapper);
}
});
return new Iterable<UniprotEntry>()
{
boolean hasIterated = false;
public Iterator<UniprotEntry> iterator()
{
try
{
if (hasIterated)
{
throw new IllegalStateException();
}
hasIterated = true;
return new Iterator<UniprotEntry>()
{
final Iterator<UniprotEntry> delegate =
parser.iterate(method.getResponseBodyAsStream());
public boolean hasNext()
{
final boolean hasNext = delegate.hasNext();
if (hasNext == false)
{
method.releaseConnection();
}
return hasNext;
}
public UniprotEntry next()
{
try
{
return delegate.next();
} catch (RuntimeException ex)
{
method.releaseConnection();
throw ex;
}
}
public void remove()
{
method.releaseConnection();
throw new UnsupportedOperationException();
}
};
} catch (IOException ex)
{
method.releaseConnection();
throw CheckedExceptionTunnel.wrapIfNecessary(ex);
}
}
};
} catch (IOException ex)
{
method.releaseConnection();
throw CheckedExceptionTunnel.wrapIfNecessary(ex);
}
}
public Iterable<UniprotEntry> queryForIds(String... keys) throws IOExceptionUnchecked
{
return queryForIds(Arrays.asList(keys));
}
public Iterable<UniprotEntry> queryForIds(List<String> keys) throws IOExceptionUnchecked
{
final String queryURL = buildQueryURLForKeys(keys);
return runQuery(queryURL);
}
/**
* Runs a query against Uniprot with the given <var>queryExpression</var>.
*
* @param queryExpression The query expression to use for the query. See <a
* href="http://www.uniprot.org/help/text-search">Uniprot Online Help</a> for details
* on the query language.
*/
public Iterable<UniprotEntry> query(String queryExpression) throws IOExceptionUnchecked
{
final String queryURL = buildQueryURLForQueryExpression(queryExpression, 0, 0);
return runQuery(queryURL);
}
/**
* Runs a query against Uniprot with the given <var>queryExpression</var>.
*
* @param queryExpression The query expression to use for the query. See <a
* href="http://www.uniprot.org/help/text-search">Uniprot Online Help</a> for details
* on the query language.
* @param limit The maximum number of result entries to return.
* @param offset The offset, that is the first result entry to return when counting starts with
* 0.
*/
public Iterable<UniprotEntry> query(String queryExpression, int limit, int offset)
throws IOExceptionUnchecked
{
final String queryURL = buildQueryURLForQueryExpression(queryExpression, limit, offset);
return runQuery(queryURL);
}
}
/*
* Copyright 2009 ETH Zuerich, CISD
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.systemsx.cisd.common.net.uniprot;
import static ch.systemsx.cisd.common.net.uniprot.UniprotColumn.ENTRY_NAME;
import static ch.systemsx.cisd.common.net.uniprot.UniprotColumn.EXISTENCE;
import static ch.systemsx.cisd.common.net.uniprot.UniprotColumn.GENES;
import static ch.systemsx.cisd.common.net.uniprot.UniprotColumn.ID;
import static ch.systemsx.cisd.common.net.uniprot.UniprotColumn.LAST_MODIFIED;
import static ch.systemsx.cisd.common.net.uniprot.UniprotColumn.LENGTH;
import static ch.systemsx.cisd.common.net.uniprot.UniprotColumn.SEQUENCE;
import static ch.systemsx.cisd.common.net.uniprot.UniprotColumn.STATUS;
import static ch.systemsx.cisd.common.net.uniprot.UniprotColumn.VERSION;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertTrue;
import java.text.ParseException;
import java.util.Date;
import org.testng.annotations.Test;
import ch.systemsx.cisd.common.logging.LogInitializer;
/**
* A simple test case for the {@link UniprotQuery}.
* <p>
* <i>Note: This test depends on the Uniprot database query engine at http://www.uniprot.org/uniprot
* !</i>
*
* @author Bernd Rinn
*/
public class UniprotQueryTest
{
private final static Date REFERENCE_DATE;
static
{
LogInitializer.init();
try
{
REFERENCE_DATE = UniprotEntry.DATE_FORMAT.get().parse("2009-11-24");
} catch (ParseException ex)
{
throw new Error(ex);
}
}
@Test(groups = "network")
public void testUniprotQuery()
{
final UniprotQuery uniprot =
new UniprotQuery(ENTRY_NAME, GENES, EXISTENCE, STATUS, LENGTH, SEQUENCE, VERSION,
LAST_MODIFIED);
int count = 0;
for (UniprotEntry entry : uniprot.queryForIds("p12345"))
{
++count;
assertEquals("P12345", entry.getId());
assertEquals("AATM_RABIT", entry.getEntryName());
assertEquals("GOT2", entry.getGenes());
assertEquals("Evidence at protein level", entry.getExistence());
assertEquals("reviewed", entry.getStatus());
assertEquals(30, entry.getLength().intValue());
assertEquals("SSWWAHVEMG PPDPILGVTE AYKRDTNSKK", entry.getSequence());
assertTrue(Integer.toString(entry.getVersion()), entry.getVersion() >= 63);
assertTrue(entry.getLastModifiedStr(),
entry.getLastModified().getTime() >= REFERENCE_DATE.getTime());
System.out.println(ID.name() + ": " + entry.getId());
System.out.println(ENTRY_NAME.name() + ": " + entry.getEntryName());
System.out.println(GENES.name() + ": " + entry.getGenes());
System.out.println(EXISTENCE.name() + ": " + entry.getExistence());
System.out.println(STATUS.name() + ": " + entry.getStatus());
System.out.println(LENGTH.name() + ": " + entry.getLength());
System.out.println(SEQUENCE.name() + ": " + entry.getSequence());
System.out.println(VERSION.name() + ": " + entry.getVersion());
System.out.println(LAST_MODIFIED.name() + ": " + entry.getLastModifiedStr());
System.out.println();
}
assertEquals(1, count);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment