diff --git a/common/source/java/ch/systemsx/cisd/common/db/DBRestrictionParser.java b/common/source/java/ch/systemsx/cisd/common/db/DBRestrictionParser.java new file mode 100644 index 0000000000000000000000000000000000000000..f30b28fdbeb6676e6660f36dbe822b20b26b6945 --- /dev/null +++ b/common/source/java/ch/systemsx/cisd/common/db/DBRestrictionParser.java @@ -0,0 +1,224 @@ +/* + * Copyright 2007 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.db; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import ch.systemsx.cisd.common.logging.LogCategory; +import ch.systemsx.cisd.common.logging.LogFactory; + +/** + * A parser for SQL92 data definition language scripts. The result are columns lengths and checked constraints. + * + * @author Bernd Rinn + */ +public class DBRestrictionParser +{ + private static final String CREATE_DOMAIN_PREFIX = "create domain "; + + private static final Pattern VARCHAR_PATTERN = Pattern.compile("(varchar|character varying)\\(([0-9]+)\\).*"); + + /** The prefix each <code>create table</code> statement starts with. */ + private static final String CREATE_TABLE_PREFIX = "create table "; + + private static final Pattern CREATE_TABLE_PATTERN = + Pattern.compile(CREATE_TABLE_PREFIX + "([a-z,0-9,_]+) \\((.+)\\)"); + + private static final Pattern NOT_NULL_TABLE_PATTERN = + Pattern.compile("\\w+ ((default .+ not null)|(not null)|(not null .+ default.+))"); + + /** The prefix each <code>alter table</code> statement starts with (to add a constraint). */ + private static final String ALTER_TABLE_PREFIX = "alter table "; + + private static final Pattern CHECK_CONSTRAINT_PATTERN = + Pattern.compile(ALTER_TABLE_PREFIX + + "([a-z,0-9,_]+) add constraint [a-z,0-9,_]+ check \\(([a-z,0-9,_]+) in \\((.+)\\)\\)"); + + private static final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION, DBRestrictionParser.class); + + // @Private + final Map<String, DBTableRestrictions> tableRestrictionMap = new HashMap<String, DBTableRestrictions>(); + + public DBRestrictionParser(String ddlScript) + { + final List<String> normalizedDDLScript = normalize(ddlScript); + final Map<String, Integer> domains = parseDomains(normalizedDDLScript); + parseColumnLength(normalizedDDLScript, domains); + parserCheckedConstraints(normalizedDDLScript); + } + + // @Private + static List<String> normalize(String ddlScript) + { + final List<String> list = new ArrayList<String>(); + final SQLCommandTokenizer normalizer = new SQLCommandTokenizer(ddlScript); + String command; + do + { + command = normalizer.getNextCommand(); + if (command != null) + { + list.add(command); + } + } while (command != null); + return list; + } + + // @Private + static Map<String, Integer> parseDomains(List<String> ddlScript) + { + final Map<String, Integer> domains = new HashMap<String, Integer>(); + for (String line : ddlScript) + { + if (line.startsWith(CREATE_DOMAIN_PREFIX)) + { + String domainDefinition = line.substring(CREATE_DOMAIN_PREFIX.length()); + int indexOfAS = domainDefinition.indexOf("as"); + if (indexOfAS < 0) + { + operationLog.warn("line \"" + line + + "\" starts like a domain definition, but key word 'AS' is missing."); + continue; + } + String domainName = domainDefinition.substring(0, indexOfAS).trim(); + domainDefinition = domainDefinition.substring(indexOfAS + 2).trim(); + final Matcher varCharMatcher = VARCHAR_PATTERN.matcher(domainDefinition); + if (varCharMatcher.matches()) + { + domains.put(domainName, Integer.parseInt(varCharMatcher.group(2))); + } + } + } + + return domains; + } + + private void parseColumnLength(List<String> ddlScript, Map<String, Integer> domains) + { + for (String line : ddlScript) + { + final Matcher createTableMatcher = CREATE_TABLE_PATTERN.matcher(line); + if (createTableMatcher.matches()) + { + final String tableName = createTableMatcher.group(1); + final String tableDefinition = createTableMatcher.group(2); + final String[] columnDefinitions = StringUtils.split(tableDefinition, ','); + for (String columnDefinition : columnDefinitions) + { + parseColumnDefinition(columnDefinition, tableName, domains); + } + } + } + } + + private void parseColumnDefinition(String columnDefinition, final String tableName, Map<String, Integer> domains) + throws NumberFormatException + { + if (columnDefinition.startsWith("constraint ")) + { + return; + } + int indexOfFirstSpace = columnDefinition.indexOf(' '); + if (indexOfFirstSpace < 0) + { + operationLog.warn("Invalid column definition \"" + columnDefinition + "\" for table " + tableName); + return; + } + String columnName = columnDefinition.substring(0, indexOfFirstSpace).trim(); + if (columnName.startsWith("\"") && columnName.endsWith("\"")) + { + columnName = columnName.substring(1, columnName.length() - 1); + } + final String typeDefinition = columnDefinition.substring(indexOfFirstSpace).trim(); + final Matcher varCharMatcher = VARCHAR_PATTERN.matcher(typeDefinition); + if (varCharMatcher.matches()) + { + getTableRestrictions(tableName).columnLengthMap.put(columnName, Integer.parseInt(varCharMatcher + .group(2))); + } else + { + final Integer domainLength = domains.get(StringUtils.split(typeDefinition, ' ')[0]); + if (domainLength != null) + { + getTableRestrictions(tableName).columnLengthMap.put(columnName, domainLength); + } + } + + if (NOT_NULL_TABLE_PATTERN.matcher(typeDefinition).matches()) + { + getTableRestrictions(tableName).notNullSet.add(columnName); + } + } + + private void parserCheckedConstraints(List<String> ddlScript) + { + for (String line : ddlScript) + { + final Matcher checkedConstraintMatcher = CHECK_CONSTRAINT_PATTERN.matcher(line); + if (checkedConstraintMatcher.matches()) + { + final String tableName = checkedConstraintMatcher.group(1); + final String columnName = checkedConstraintMatcher.group(2); + final String alternativesStr = checkedConstraintMatcher.group(3); + final String[] alternatives = StringUtils.split(alternativesStr, ','); + final Set<String> alternativeSet = new HashSet<String>(); + for (String alternative : alternatives) + { + if (alternative.charAt(0) != '\'' || alternative.charAt(alternative.length() - 1) != '\'') + { + operationLog.warn("Invalid alternatives definition \"" + alternative + "\" for column " + + columnName + " of table " + tableName); + continue; + } + alternativeSet.add(alternative.substring(1, alternative.length() - 1)); + } + getTableRestrictions(tableName).checkedConstraintsMap.put(columnName, alternativeSet); + } + } + } + + /** + * @return The table restrictions of <var>tableName</var> + */ + // @Private + DBTableRestrictions getTableRestrictions(String tableName) + { + DBTableRestrictions table = tableRestrictionMap.get(tableName); + if (table == null) + { + table = new DBTableRestrictions(); + tableRestrictionMap.put(tableName, table); + } + return table; + } + + public Map<String, DBTableRestrictions> getDBRestrictions() + { + return Collections.unmodifiableMap(tableRestrictionMap); + } + +} diff --git a/common/source/java/ch/systemsx/cisd/common/db/DBTableRestrictions.java b/common/source/java/ch/systemsx/cisd/common/db/DBTableRestrictions.java new file mode 100644 index 0000000000000000000000000000000000000000..ae1d5c5a72f94339ed11c22f695200455b799a0c --- /dev/null +++ b/common/source/java/ch/systemsx/cisd/common/db/DBTableRestrictions.java @@ -0,0 +1,55 @@ +/* + * Copyright 2007 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.db; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * A class that holds the restrictions put upon columns in a database table. + * + * @author Bernd Rinn + */ +public class DBTableRestrictions +{ + final Map<String, Integer> columnLengthMap = new HashMap<String, Integer>(); + + final Map<String, Set<String>> checkedConstraintsMap = new HashMap<String, Set<String>>(); + + final Set<String> notNullSet = new HashSet<String>(); + + public int getLength(String columnName) + { + final Integer columnLength = columnLengthMap.get(columnName); + assert columnLength != null : "Illegal column '" + columnName +"'."; + return columnLength; + } + + public Set<String> tryGetCheckedConstaint(String columnName) + { + final Set<String> checkedConstraint = checkedConstraintsMap.get(columnName); + return (checkedConstraint == null) ? null : Collections.unmodifiableSet(checkedConstraint); + } + + public boolean hasNotNullConstraint(String columnName) + { + return notNullSet.contains(columnName); + } +} \ No newline at end of file diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/db/DBRestrictionParserTest.java b/common/sourceTest/java/ch/systemsx/cisd/common/db/DBRestrictionParserTest.java new file mode 100644 index 0000000000000000000000000000000000000000..8ae9910cdaa149188392644cf1197eee6ac51b65 --- /dev/null +++ b/common/sourceTest/java/ch/systemsx/cisd/common/db/DBRestrictionParserTest.java @@ -0,0 +1,188 @@ +/* + * Copyright 2008 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.db; + +import static org.testng.AssertJUnit.*; + +import java.io.File; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import ch.systemsx.cisd.common.logging.LogCategory; +import ch.systemsx.cisd.common.logging.LogInitializer; +import ch.systemsx.cisd.common.logging.LogMonitoringAppender; +import ch.systemsx.cisd.common.utilities.FileUtilities; + +/** + * Test cases for the @{link DBREstrictionParser}. + * + * @author Bernd Rinn + */ +public class DBRestrictionParserTest +{ + + private String sqlScript; + + @BeforeClass + public void setup() + { + LogInitializer.init(); + sqlScript = + FileUtilities.loadToString(new File( + "sourceTest/java/ch/systemsx/cisd/common/db/DBRestrictionsTest.sql")); + assert sqlScript != null; + } + + @Test + public void testNormalize() + { + final List<String> normalizedList = DBRestrictionParser.normalize(" a 1 ;; B\t2;\n\nC; \n--D\n E "); + assertEquals(Arrays.asList("a 1", "b 2", "c", "e"), normalizedList); + } + + @Test + public void testGetDomains() + { + String invalidDomainStatement = "create domain bla for varchar(0)"; + final List<String> domainScript = + Arrays.asList("create table sometable", "create domain user_id as varchar(15)", + invalidDomainStatement, "create domain code as varchar(8)", + "create domain description_80 as varchar(81)"); + final LogMonitoringAppender appender = + LogMonitoringAppender.addAppender(LogCategory.OPERATION, "line \"" + invalidDomainStatement + + "\" starts like a domain definition, but key word 'AS' is missing."); + try + { + final Map<String, Integer> domains = DBRestrictionParser.parseDomains(domainScript); + appender.verifyLogHasHappened(); + final Map<String, Integer> expectedDomains = new HashMap<String, Integer>(); + expectedDomains.put("user_id", 15); + expectedDomains.put("code", 8); + expectedDomains.put("description_80", 81); + assertEquals(expectedDomains, domains); + } finally + { + LogMonitoringAppender.removeAppender(appender); + } + } + + @Test + public void testDefaultKeywordInDomain() + { + final List<String> domainScript = + Arrays.asList("create domain vc22 as varchar(22) default 'nothing special'"); + + final LogMonitoringAppender appender = LogMonitoringAppender.addAppender(LogCategory.OPERATION, "ill-formed"); + try + { + final Map<String, Integer> domains = DBRestrictionParser.parseDomains(domainScript); + appender.verifyLogHasNotHappened(); + assertNotNull(domains.get("vc22")); + assertEquals(22, domains.get("vc22").intValue()); + } finally + { + LogMonitoringAppender.removeAppender(appender); + } + } + + @Test + public void testDoublePrecisionInDomain() + { + final List<String> domainScript = + Arrays.asList("create domain dp as double precision"); + + final LogMonitoringAppender appender = LogMonitoringAppender.addAppender(LogCategory.OPERATION, "ill-formed"); + try + { + final Map<String, Integer> domains = DBRestrictionParser.parseDomains(domainScript); + appender.verifyLogHasNotHappened(); + assertTrue(domains.isEmpty()); + } finally + { + LogMonitoringAppender.removeAppender(appender); + } + } + + @Test + public void testDoublePrecisionAndDefaultInDomain() + { + final List<String> domainScript = + Arrays.asList("create domain dp as double precision default 3.14159"); + + final LogMonitoringAppender appender = LogMonitoringAppender.addAppender(LogCategory.OPERATION, "ill-formed"); + try + { + final Map<String, Integer> domains = DBRestrictionParser.parseDomains(domainScript); + appender.verifyLogHasNotHappened(); + assertTrue(domains.isEmpty()); + } finally + { + LogMonitoringAppender.removeAppender(appender); + } + } + + @Test + public void testColumnLengths() + { + final DBRestrictionParser parser = new DBRestrictionParser(sqlScript); + + assertEquals(10, parser.getTableRestrictions("contacts").getLength("cnta_type")); + assertEquals(30, parser.getTableRestrictions("contacts").getLength("firstname")); + assertEquals(1, parser.getTableRestrictions("contacts").getLength("midinitial")); + assertEquals(30, parser.getTableRestrictions("contacts").getLength("lastname")); + assertEquals(50, parser.getTableRestrictions("contacts").getLength("email")); + assertEquals(15, parser.getTableRestrictions("contacts").getLength("user_id")); + + assertEquals(8, parser.getTableRestrictions("material_types").getLength("code")); + assertEquals(80, parser.getTableRestrictions("material_types").getLength("description")); + + assertEquals(50, parser.getTableRestrictions("materials").getLength("name")); + assertEquals(4, parser.getTableRestrictions("materials").getLength("mate_sub_type")); + } + + @Test(expectedExceptions = AssertionError.class) + public void testInvalidTable() + { + final DBRestrictionParser parser = new DBRestrictionParser(""); + assertEquals(Integer.MAX_VALUE, parser.getTableRestrictions("doesnotexit").getLength("doesnotexist")); + } + + @Test(expectedExceptions = AssertionError.class) + public void testInvalidColumn() + { + final DBRestrictionParser parser = new DBRestrictionParser("create table tab (a integer, b varchar(1))"); + assertEquals(Integer.MAX_VALUE, parser.getTableRestrictions("tab").getLength("doesnotexist")); + } + + @Test + public void testCheckedConstraints() + { + final DBRestrictionParser parser = new DBRestrictionParser(sqlScript); + + assertEquals(new HashSet<String>(Arrays.asList("PERS", "ORGA")), parser.getTableRestrictions("contacts") + .tryGetCheckedConstaint("cnta_type")); + assertEquals(new HashSet<String>(Arrays.asList("STOB", "MATE")), parser.getTableRestrictions("materials") + .tryGetCheckedConstaint("mate_sub_type")); + } + +} diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/db/DBRestrictionsTest.sql b/common/sourceTest/java/ch/systemsx/cisd/common/db/DBRestrictionsTest.sql new file mode 100644 index 0000000000000000000000000000000000000000..51d31d1cbf9f9174117d9f69f902ec9f469e0c20 --- /dev/null +++ b/common/sourceTest/java/ch/systemsx/cisd/common/db/DBRestrictionsTest.sql @@ -0,0 +1,59 @@ +-- D:\DDL\postgresql\3V_LIMS_Version_1.sql +-- +-- Generated for ANSI SQL92 on Thu May 10 15:50:21 2007 by Server Generator 10.1.2.6.18 + + + CREATE DOMAIN DESCRIPTION_80 AS VARCHAR(80); +CREATE DOMAIN USER_ID AS VARCHAR(15); +CREATE DOMAIN TECH_ID AS INTEGER; +CREATE DOMAIN OBJECT_NAME AS VARCHAR(50); +CREATE DOMAIN CODE AS VARCHAR(8); +CREATE DOMAIN DESCRIPTION_250 AS VARCHAR(250); +CREATE TABLE EXPERIMENT_TYPES (CODE CODE NOT NULL,DESCRIPTION +DESCRIPTION_80 NOT NULL) ; +CREATE TABLE CONTACTS (ID TECH_ID NOT NULL,CNTA_TYPE VARCHAR(10) DEFAULT 'PERS' NOT NULL,FIRSTNAME VARCHAR(30),MIDINITIAL VARCHAR(1),LASTNAME VARCHAR(30),EMAIL VARCHAR(50),USER_ID USER_ID,CNTA_ID_ORGANIZATION TECH_ID,ORGANIZATION_NAME OBJECT_NAME,DESCRIPTION DESCRIPTION_80) ; +CREATE TABLE MATERIAL_TYPES (CODE CODE NOT NULL,"DESCRIPTION" DESCRIPTION_80 NOT NULL) ; +CREATE TABLE MATERIALS (ID TECH_ID NOT NULL,NAME OBJECT_NAME,MATE_SUB_TYPE VARCHAR(4) NOT NULL DEFAULT 'MATE',MATY_CODE CODE NOT NULL,DESCRIPTION DESCRIPTION_250) ; +CREATE TABLE EXPERIMENTS ( + ID TECH_ID NOT NULL, + NAME OBJECT_NAME, + CNTA_ID_REGISTERER TECH_ID NOT NULL, + MATE_ID_STUDY_OBJECT TECH_ID NOT NULL, + EXTY_CODE CODE NOT NULL, + REGISTRATION_DATE DATE, + DESCRIPTION DESCRIPTION_250, + CONSTRAINT data_samp_arc_ck CHECK ((samp_id_acquired_from IS NOT NULL) AND (samp_id_derived_from IS NULL)); +); + +ALTER TABLE EXPERIMENT_TYPES ADD CONSTRAINT EXTY_PK PRIMARY KEY(CODE); +ALTER TABLE CONTACTS ADD CONSTRAINT CNTA_PK PRIMARY KEY(ID); +ALTER TABLE MATERIAL_TYPES ADD CONSTRAINT MATY_PK PRIMARY KEY(CODE); +ALTER TABLE MATERIALS ADD CONSTRAINT MATE_PK PRIMARY KEY(ID); +ALTER TABLE EXPERIMENTS ADD CONSTRAINT EXPE_PK PRIMARY KEY(ID); + +ALTER TABLE CONTACTS ADD CONSTRAINT AVCON_1178805021_CNTA__000 CHECK (CNTA_TYPE IN + ('PERS', + 'ORGA') +) ; +ALTER TABLE MATERIALS ADD CONSTRAINT AVCON_1178805021_MATE__000 CHECK (MATE_SUB_TYPE IN ('MATE', 'STOB')) ; +ALTER TABLE CONTACTS ADD CONSTRAINT CNTA_CNTA_ORGANIZATION_FK FOREIGN KEY (CNTA_ID_ORGANIZATION)REFERENCES CONTACTS(ID); +ALTER TABLE MATERIALS ADD CONSTRAINT MATE_MATY_FK FOREIGN KEY (MATY_CODE)REFERENCES MATERIAL_TYPES(CODE); +ALTER TABLE EXPERIMENTS ADD CONSTRAINT EXPE_CNTA_FK FOREIGN KEY (CNTA_ID_REGISTERER)REFERENCES CONTACTS(ID); +ALTER TABLE EXPERIMENTS ADD CONSTRAINT EXPE_MATE_FK FOREIGN KEY (MATE_ID_STUDY_OBJECT)REFERENCES MATERIALS(ID); +ALTER TABLE EXPERIMENTS ADD CONSTRAINT EXPE_EXTY_FK FOREIGN KEY (EXTY_CODE)REFERENCES EXPERIMENT_TYPES(CODE); + +-- D:\DDL\oracle\3V_LIMS_Version_1.sqs +-- +-- Generated for Oracle 9i on Thu May 10 15:49:25 2007 by Server Generator 10.1.2.6.18 + +-- Creating Sequence 'CONTACT_ID_SEQ' +CREATE SEQUENCE CONTACT_ID_SEQ; + +-- Creating Sequence 'EXPERIMENT_ID_SEQ' +CREATE SEQUENCE EXPERIMENT_ID_SEQ; + +-- Creating Sequence 'MATERIAL_ID_SEQ' +CREATE SEQUENCE MATERIAL_ID_SEQ; + + +