/* * * * Copyright 2014 Orient Technologies LTD (info(at)orientechnologies.com) * * * * 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. * * * * For more information: http://www.orientechnologies.com * */ package com.orientechnologies.orient.core.sql; import com.orientechnologies.orient.core.command.OCommandDistributedReplicateRequest; import com.orientechnologies.orient.core.command.OCommandRequest; import com.orientechnologies.orient.core.command.OCommandRequestText; import com.orientechnologies.orient.core.config.OGlobalConfiguration; import com.orientechnologies.orient.core.db.document.ODatabaseDocument; import com.orientechnologies.orient.core.exception.OCommandExecutionException; import com.orientechnologies.orient.core.metadata.schema.OClass; import com.orientechnologies.orient.core.metadata.schema.OClassImpl; import com.orientechnologies.orient.core.metadata.schema.OPropertyImpl; import com.orientechnologies.orient.core.metadata.schema.OType; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * SQL CREATE PROPERTY command: Creates a new property in the target class. * * @author Luca Garulli * @author Michael MacFadden */ @SuppressWarnings("unchecked") public class OCommandExecutorSQLCreateProperty extends OCommandExecutorSQLAbstract implements OCommandDistributedReplicateRequest { public static final String KEYWORD_CREATE = "CREATE"; public static final String KEYWORD_PROPERTY = "PROPERTY"; public static final String KEYWORD_MANDATORY = "MANDATORY"; public static final String KEYWORD_READONLY = "READONLY"; public static final String KEYWORD_NOTNULL = "NOTNULL"; public static final String KEYWORD_MIN = "MIN"; public static final String KEYWORD_MAX = "MAX"; public static final String KEYWORD_DEFAULT = "DEFAULT"; public static final String KEYWORD_COLLATE = "COLLATE"; public static final String KEYWORD_REGEX = "REGEX"; private String className; private String fieldName; private boolean ifNotExists = false; private OType type; private String linked; private boolean readonly = false; private boolean mandatory = false; private boolean notnull = false; private String max = null; private String min = null; private String defaultValue = null; private String collate = null; private String regex = null; private boolean unsafe = false; public OCommandExecutorSQLCreateProperty parse(final OCommandRequest iRequest) { final OCommandRequestText textRequest = (OCommandRequestText) iRequest; String queryText = textRequest.getText(); String originalQuery = queryText; try { queryText = preParse(queryText, iRequest); textRequest.setText(queryText); init((OCommandRequestText) iRequest); final StringBuilder word = new StringBuilder(); int oldPos = 0; int pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true); if (pos == -1 || !word.toString().equals(KEYWORD_CREATE)) throw new OCommandSQLParsingException("Keyword " + KEYWORD_CREATE + " not found", parserText, oldPos); oldPos = pos; pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true); if (pos == -1 || !word.toString().equals(KEYWORD_PROPERTY)) throw new OCommandSQLParsingException("Keyword " + KEYWORD_PROPERTY + " not found", parserText, oldPos); oldPos = pos; pos = nextWord(parserText, parserTextUpperCase, oldPos, word, false); if (pos == -1) throw new OCommandSQLParsingException("Expected <class>.<property>", parserText, oldPos); String[] parts = split(word); if (parts.length != 2) throw new OCommandSQLParsingException("Expected <class>.<property>", parserText, oldPos); className = decodeClassName(parts[0]); if (className == null) throw new OCommandSQLParsingException("Class not found", parserText, oldPos); fieldName = decodeClassName(parts[1]); oldPos = pos; pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true); if (pos == -1) throw new OCommandSQLParsingException("Missed property type", parserText, oldPos); if("IF".equalsIgnoreCase(word.toString())){ oldPos = pos; pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true); if (pos == -1) throw new OCommandSQLParsingException("Missed property type", parserText, oldPos); if(!"NOT".equalsIgnoreCase(word.toString())){ throw new OCommandSQLParsingException("Expected NOT EXISTS after IF", parserText, oldPos); } oldPos = pos; pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true); if (pos == -1) throw new OCommandSQLParsingException("Missed property type", parserText, oldPos); if(!"EXISTS".equalsIgnoreCase(word.toString())){ throw new OCommandSQLParsingException("Expected EXISTS after IF NOT", parserText, oldPos); } this.ifNotExists = true; oldPos = pos; pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true); } type = OType.valueOf(word.toString()); // Use a REGEX for the rest because we know exactly what we are looking for. // If we are in strict mode, the parser took care of strict matching. String rest = parserText.substring(pos).trim(); String pattern = "(`[^`]*`|[^\\(]\\S*)?\\s*(\\(.*\\))?\\s*(UNSAFE)?"; Pattern r = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE); Matcher m = r.matcher(rest.trim()); if (m.matches()) { // Linked Type / Class if (m.group(1) != null && !m.group(1).equalsIgnoreCase("UNSAFE")) { linked = m.group(1); if (linked.startsWith("`") && linked.endsWith("`") && linked.length() > 1) { linked = linked.substring(1, linked.length() - 1); } } // Attributes if (m.group(2) != null) { String raw = m.group(2); String atts = raw.substring(1, raw.length() - 1); processAtts(atts); } // UNSAFE if (m.group(3) != null) { this.unsafe = true; } } else { // Syntax Error } } finally { textRequest.setText(originalQuery); } return this; } private void processAtts(String atts) { String[] split = atts.split(","); for (String attDef: split) { String[] parts = attDef.trim().split("\\s+"); if (parts.length > 2) { onInvalidAttributeDefinition(attDef); } String att = parts[0].trim(); if (att.equalsIgnoreCase(KEYWORD_MANDATORY)) { this.mandatory = getOptionalBoolean(parts); } else if (att.equalsIgnoreCase(KEYWORD_READONLY)) { this.readonly = getOptionalBoolean(parts); } else if (att.equalsIgnoreCase(KEYWORD_NOTNULL)) { this.notnull = getOptionalBoolean(parts); } else if (att.equalsIgnoreCase(KEYWORD_MIN)) { this.min = getRequiredValue(attDef, parts); } else if (att.equalsIgnoreCase(KEYWORD_MAX)) { this.max = getRequiredValue(attDef, parts); } else if (att.equalsIgnoreCase(KEYWORD_DEFAULT)) { this.defaultValue = getRequiredValue(attDef, parts); } else if (att.equalsIgnoreCase(KEYWORD_COLLATE)) { this.collate = getRequiredValue(attDef, parts); } else if (att.equalsIgnoreCase(KEYWORD_REGEX)) { this.regex = getRequiredValue(attDef, parts); } else { onInvalidAttributeDefinition(attDef); } } } private void onInvalidAttributeDefinition(String attDef) { throw new OCommandSQLParsingException("Invalid attribute definition: '" + attDef + "'"); } private boolean getOptionalBoolean(String[] parts) { if (parts.length < 2) { return true; } String trimmed = parts[1].trim(); if (trimmed.length() == 0) { return true; } return Boolean.parseBoolean(trimmed); } private String getRequiredValue(String attDef, String[] parts) { if (parts.length < 2) { onInvalidAttributeDefinition(attDef); } String value = parts[1].trim(); if (value.length() == 0) { onInvalidAttributeDefinition(attDef); } if (value.equalsIgnoreCase("null")) { value = null; } if (value != null && isQuoted(value)) { value = removeQuotes(value); } return value; } private String[] split(StringBuilder word) { List<String> result = new ArrayList<String>(); StringBuilder builder = new StringBuilder(); boolean quoted = false; for (char c : word.toString().toCharArray()) { if (!quoted) { if (c == '`') { quoted = true; } else if (c == '.') { String nextToken = builder.toString().trim(); if (nextToken.length() > 0) { result.add(nextToken); } builder = new StringBuilder(); } else { builder.append(c); } } else { if (c == '`') { quoted = false; } else { builder.append(c); } } } String nextToken = builder.toString().trim(); if (nextToken.length() > 0) { result.add(nextToken); } return result.toArray(new String[] {}); } @Override public long getDistributedTimeout() { return OGlobalConfiguration.DISTRIBUTED_COMMAND_QUICK_TASK_SYNCH_TIMEOUT.getValueAsLong(); } /** * Execute the CREATE PROPERTY. */ public Object execute(final Map<Object, Object> iArgs) { if (type == null) throw new OCommandExecutionException("Cannot execute the command because it has not been parsed yet"); final ODatabaseDocument database = getDatabase(); final OClassImpl sourceClass = (OClassImpl) database.getMetadata().getSchema().getClass(className); if (sourceClass == null) throw new OCommandExecutionException("Source class '" + className + "' not found"); OPropertyImpl prop = (OPropertyImpl) sourceClass.getProperty(fieldName); if (prop != null) { if(ifNotExists){ return sourceClass.properties().size(); } throw new OCommandExecutionException( "Property '" + className + "." + fieldName + "' already exists. Remove it before to retry."); } // CREATE THE PROPERTY OClass linkedClass = null; OType linkedType = null; if (linked != null) { // FIRST SEARCH BETWEEN CLASSES linkedClass = database.getMetadata().getSchema().getClass(linked); if (linkedClass == null) // NOT FOUND: SEARCH BETWEEN TYPES linkedType = OType.valueOf(linked.toUpperCase(Locale.ENGLISH)); } // CREATE IT LOCALLY OPropertyImpl internalProp = sourceClass.addPropertyInternal(fieldName, type, linkedType, linkedClass, unsafe); boolean toSave = false; if (readonly) { internalProp.setReadonly(true); toSave = true; } if (mandatory) { internalProp.setMandatory(true); toSave = true; } if (notnull) { internalProp.setNotNull(true); toSave = true; } if (max != null) { internalProp.setMax(max); toSave = true; } if (min != null) { internalProp.setMin(min); toSave = true; } if (defaultValue != null) { internalProp.setDefaultValue(defaultValue); toSave = true; } if (collate != null) { internalProp.setCollate(collate); toSave = true; } if (regex != null) { internalProp.setRegexp(regex); toSave = true; } if(toSave) { internalProp.save(); } return sourceClass.properties().size(); } private String removeQuotes(String s) { s = s.trim(); return s.substring(1, s.length() - 1).replaceAll("\\\\\"", "\""); } private boolean isQuoted(String s) { s = s.trim(); if (s.startsWith("\"") && s.endsWith("\"")) return true; if (s.startsWith("'") && s.endsWith("'")) return true; if (s.startsWith("`") && s.endsWith("`")) return true; return false; } @Override public QUORUM_TYPE getQuorumType() { return QUORUM_TYPE.ALL; } @Override public String getUndoCommand() { return "drop property " + className + "." + fieldName; } @Override public String getSyntax() { return "CREATE PROPERTY <class>.<property> [IF NOT EXISTS] <type> [<linked-type>|<linked-class>] " + "[(mandatory <true|false>, notnull <true|false>, <true|false>, default <value>, min <value>, max <value>)] " + "[UNSAFE]"; } }