001/*-----------------------------------------------------------------------+
002 | eu.cqse.check.abap
003 |                                                                       |
004   $Id: OpenSqlStatement.java 12402 2016-08-05 06:23:07Z roehm $
005 |                                                                       |
006 | Copyright (c)  2009-2016 CQSE GmbH                                 |
007 +-----------------------------------------------------------------------*/
008package eu.cqse.check.abap;
009
010import static eu.cqse.check.framework.scanner.ETokenType.DELETE;
011import static eu.cqse.check.framework.scanner.ETokenType.FROM;
012import static eu.cqse.check.framework.scanner.ETokenType.INSERT;
013import static eu.cqse.check.framework.scanner.ETokenType.INTO;
014import static eu.cqse.check.framework.shallowparser.TokenStreamUtils.startsWith;
015
016import java.util.EnumSet;
017import java.util.List;
018
019import org.conqat.lib.commons.collections.UnmodifiableList;
020import org.conqat.lib.commons.string.StringUtils;
021
022import eu.cqse.check.framework.scanner.ETokenType;
023import eu.cqse.check.framework.scanner.ETokenType.ETokenClass;
024import eu.cqse.check.framework.scanner.IToken;
025import eu.cqse.check.framework.shallowparser.TokenStreamUtils;
026import eu.cqse.check.framework.shallowparser.framework.ShallowEntity;
027import eu.cqse.check.framework.typetracker.ITypeResolution;
028import eu.cqse.check.framework.util.abap.AbapCheckUtils;
029import eu.cqse.check.framework.util.abap.AbapLanguageFeatureParser;
030
031/**
032 * Representation of a SQL-like ABAP statement that modifies database table's
033 * contents. This offers more detailed analysis, e.g. finding out whether the
034 * targeted table is in the standard namespace.
035 */
036public abstract class OpenSqlWriteStatement {
037
038        /** The tokens that constitute the statement. */
039        protected List<IToken> tokens;
040
041        /**
042         * An entity representing an OpenSQL statement directly or an entity
043         * representing a macro which contains an OpenSQL statement.
044         */
045        protected ShallowEntity entity;
046
047        /** Type resolution of OpenSQL statement. */
048        protected ITypeResolution typeResolution;
049
050        /** The token types for '->' and '=>', which indicate class member access. */
051        private static final EnumSet<ETokenType> MEMBER_ARROWS = EnumSet.of(ETokenType.ARROW, ETokenType.EQGT);
052
053        /**
054         * Constructor.
055         */
056        private OpenSqlWriteStatement(ShallowEntity entity, List<IToken> tokens, ITypeResolution typeResolution) {
057                this.entity = entity;
058                this.tokens = new UnmodifiableList<>(tokens);
059                this.typeResolution = typeResolution;
060        }
061
062        /**
063         * Determines the token index at which the table name starts. Returns 1 by
064         * default.
065         */
066        protected int getTableTokenIndex() {
067                return 1;
068        }
069
070        /**
071         * Checks whether the statement is targeted at an internal table. Returns true
072         * if it is sure from the statement syntax alone that an iTab is targeted.
073         */
074        protected abstract boolean hasSureITabSyntax();
075
076        /** Checks whether the target table of the statement is a variable. */
077        public boolean targetsInternalTable() {
078
079                if (hasSureITabSyntax()) {
080                        return true;
081                }
082
083                int nameStartIndex = getTableTokenIndex();
084                if (isFieldSymbolStart(nameStartIndex) || isClassMemberStart(nameStartIndex)) {
085                        return true;
086                }
087
088                // Anything longer than 16 characters cannot be a DB table. Also ignore
089                // representation of table names in variable or expression (i.e. the table name
090                // starts with '(') .
091                String tableName = tokens.get(nameStartIndex).getText();
092                if (tableName.length() > 16 || tableName.startsWith("(")) {
093                        return true;
094                }
095
096                return AbapCheckUtils.isVariable(tableName, entity, typeResolution);
097        }
098
099        /** Gets the statement start token, e.g. INSERT. */
100        public IToken getStatementStartToken() {
101                return tokens.get(0);
102        }
103
104        /** Returns the name of the targeted table in upper case. */
105        public String getTableName() {
106                return getTableNameToken().getText().toUpperCase();
107        }
108
109        /** Returns the {@link IToken} holding the table name */
110        public IToken getTableNameToken() {
111                return tokens.get(getTableTokenIndex());
112        }
113
114        /**
115         * Whether the given token index is the start of a field symbol, i.e. it is a
116         * '<' followed by an identifier and a '>'.
117         */
118        private boolean isFieldSymbolStart(int index) {
119                if (tokens.size() <= index + 2) {
120                        return false;
121                }
122                return tokens.get(index).getType() == ETokenType.LT && tokens.get(index + 2).getType() == ETokenType.GT;
123        }
124
125        /**
126         * Whether the given token index is the start of a class member access, i.e. it
127         * is an identifier followed by either '->' or '=>' and another identifier.
128         */
129        private boolean isClassMemberStart(int index) {
130                if (tokens.size() <= index + 2) {
131                        return false;
132                }
133                return MEMBER_ARROWS.contains(tokens.get(index + 1).getType());
134        }
135
136        /**
137         * Checks whether the statement targets a standard database table, i.e. a
138         * database table from an SAP namespace. Uses the {@link ITypeResolution} to
139         * distinguish local variables from standard tables.
140         */
141        public boolean targetsStandardDatabaseTable() {
142                if (targetsInternalTable()) {
143                        return false;
144                }
145
146                String currentTableName = AbapLanguageFeatureParser.normalizeVariable(getTableName());
147
148                // Consider customer name space
149                if (StringUtils.startsWithOneOf(currentTableName, "/", "y", "z")) {
150                        return false;
151                }
152
153                // Internal tables EXTRACT and TOTAL are treated as non-standard tables
154                // as they can be manipulated by custom code.
155                return !(currentTableName.equals("extract") || currentTableName.equals("total"));
156        }
157
158        /**
159         * Factory method. {@code tokens} must be one of INSERT, MODIFY, UPDATE, DELETE.
160         * Returns {@code null} if passed token list is not an OpenSQL statement
161         * targeting a database table.
162         * 
163         * @throws IllegalArgumentException
164         *             In case the statement has an unsupported type.
165         */
166        public static OpenSqlWriteStatement create(ShallowEntity entity, List<IToken> tokens,
167                        ITypeResolution typeResolution) {
168                ETokenType type = tokens.get(0).getType();
169                if (tokens.size() < 2 || !EnumSet.of(ETokenClass.IDENTIFIER, ETokenClass.KEYWORD)
170                                .contains(tokens.get(1).getType().getTokenClass())) {
171                        return null;
172                }
173                switch (type) {
174                case INSERT:
175                        if (TokenStreamUtils.startsWith(tokens, ETokenType.INSERT, ETokenType.REPORT)
176                                        || TokenStreamUtils.startsWith(tokens, ETokenType.INSERT, ETokenType.LPAREN)
177                                        || TokenStreamUtils.startsWith(tokens, ETokenType.INSERT, ETokenType.TEXTPOOL)) {
178                                return null;
179                        }
180                        return filterInternal(new TableInsert(entity, tokens, typeResolution));
181                case MODIFY:
182                        if (TokenStreamUtils.startsWith(tokens, ETokenType.MODIFY, ETokenType.SCREEN)
183                                        || TokenStreamUtils.startsWith(tokens, ETokenType.MODIFY, ETokenType.LINE)) {
184                                return null;
185                        }
186                        return filterInternal(new TableModify(entity, tokens, typeResolution));
187                case UPDATE:
188                        return filterInternal(new TableUpdate(entity, tokens, typeResolution));
189                case DELETE:
190                        if (TokenStreamUtils.startsWith(tokens, ETokenType.DELETE, ETokenType.DATASET)
191                                        || TokenStreamUtils.startsWith(tokens, ETokenType.DELETE, ETokenType.DYNPRO)
192                                        || TokenStreamUtils.startsWith(tokens, ETokenType.DELETE, ETokenType.REPORT)
193                                        || TokenStreamUtils.startsWith(tokens, ETokenType.DELETE, ETokenType.TEXTPOOL)
194                                        || TokenStreamUtils.startsWith(tokens, ETokenType.DELETE, ETokenType.FROM, ETokenType.SHARED,
195                                                        ETokenType.BUFFER)
196                                        || TokenStreamUtils.startsWith(tokens, ETokenType.DELETE, ETokenType.FROM, ETokenType.SHARED,
197                                                        ETokenType.MEMORY)
198                                        || TokenStreamUtils.startsWith(tokens, ETokenType.DELETE, ETokenType.FROM, ETokenType.MEMORY)) {
199                                return null;
200                        }
201                        return filterInternal(new TableDelete(entity, tokens, typeResolution));
202                default:
203                        throw new IllegalArgumentException("Unsupported statement type " + type);
204                }
205        }
206
207        /**
208         * Returns the provided statement only if it targets a database table, otherwise
209         * returns <code>null</code>.
210         */
211        private static OpenSqlWriteStatement filterInternal(OpenSqlWriteStatement statement) {
212                if (statement.targetsInternalTable()) {
213                        return null;
214                }
215                return statement;
216        }
217
218        /** DELETE */
219        private static class TableDelete extends OpenSqlWriteStatement {
220
221                /** Constructor. */
222                public TableDelete(ShallowEntity entity, List<IToken> tokens, ITypeResolution typeResolution) {
223                        super(entity, tokens, typeResolution);
224                }
225
226                /** {@inheritDoc} */
227                @Override
228                protected int getTableTokenIndex() {
229                        if (startsWith(tokens, DELETE, FROM)) {
230                                return 2;
231                        }
232                        return 1;
233                }
234
235                /**
236                 * Checks whether a DELETE statement targets an internal table based on its
237                 * syntax.<br>
238                 * A DELETE statement is categorized as DELETE itab statement if it meets one of
239                 * the following criteria:
240                 * <ul>
241                 * <li>has tokens DELETE TABLE directly after each other
242                 * <li>has tokens DELETE ADJACENT DUPLICATES
243                 * <li>has tokens DELETE and INDEX
244                 * <li>has tokens DELETE and USING KEY
245                 * <li>if it has a FROM token, the next token after FROM represents a number
246                 * </ul>
247                 */
248                @Override
249                public boolean hasSureITabSyntax() {
250                        if (TokenStreamUtils.startsWith(tokens, ETokenType.DELETE, ETokenType.TABLE)
251                                        || TokenStreamUtils.startsWith(tokens, ETokenType.DELETE, ETokenType.ADJACENT,
252                                                        ETokenType.DUPLICATES)
253                                        || TokenStreamUtils.containsAny(tokens, ETokenType.INDEX) || TokenStreamUtils
254                                                        .containsSequence(tokens, 0, tokens.size() - 1, ETokenType.USING, ETokenType.KEY)) {
255                                return true;
256                        }
257
258                        int indexOfFrom = TokenStreamUtils.firstTokenOfType(tokens, ETokenType.FROM);
259                        if (indexOfFrom != TokenStreamUtils.NOT_FOUND) {
260                                // if token after FROM is a number, it is the itab statement
261                                // 'DELETE itab FROM idx1'
262                                String text = tokens.get(indexOfFrom + 1).getText();
263                                return text.chars().allMatch(Character::isDigit);
264                        }
265
266                        return false;
267                }
268        }
269
270        /** INSERT */
271        private static class TableInsert extends OpenSqlWriteStatement {
272                /** Constructor. */
273                public TableInsert(ShallowEntity entity, List<IToken> tokens, ITypeResolution typeResolution) {
274                        super(entity, tokens, typeResolution);
275                }
276
277                /** {@inheritDoc} */
278                @Override
279                protected int getTableTokenIndex() {
280                        if (startsWith(tokens, INSERT, INTO)) {
281                                return 2;
282                        }
283                        return 1;
284                }
285
286                /**
287                 * Checks whether an INSERT statement targets an internal table based on its
288                 * syntax.<br>
289                 * An insert statement is categorized as INSERT itab statement when it meets one
290                 * of the following criteria:
291                 * <ul>
292                 * <li>If it has tokens 'INSERT' and 'INTO', but not consecutively.
293                 * <li>has tokens INSERT and INDEX
294                 * </ul>
295                 * This also addresses an INSERT into a field-group, e.g. INSERT field_name INTO
296                 * field_group_name.
297                 */
298                @Override
299                public boolean hasSureITabSyntax() {
300                        boolean hasInsertIntoConsecutive = TokenStreamUtils.startsWith(tokens, ETokenType.INSERT, ETokenType.INTO);
301                        boolean hasInsertIntoAnywhere = TokenStreamUtils.containsAll(tokens, 0, tokens.size() - 1,
302                                        ETokenType.INSERT, ETokenType.INTO);
303                        return ((hasInsertIntoAnywhere && !hasInsertIntoConsecutive)
304                                        || TokenStreamUtils.containsAny(tokens, ETokenType.INDEX));
305                }
306        }
307
308        /** MODIFY */
309        private static class TableModify extends OpenSqlWriteStatement {
310
311                /** Constructor. */
312                protected TableModify(ShallowEntity entity, List<IToken> tokens, ITypeResolution typeResolution) {
313                        super(entity, tokens, typeResolution);
314                }
315
316                /**
317                 * Checks whether a MODIFY statement targets an internal table based on its
318                 * syntax.<br>
319                 * A MODIFY statement is categorized as MODIFY itab statement if it meets one of
320                 * the following criteria:
321                 * <ul>
322                 * <li>has tokens MODIFY TABLE directly after each other
323                 * <li>has tokens MODIFY and INDEX
324                 * <li>has tokens MODIFY and TRANSPORTING
325                 * <li>has tokens MODIFY and ASSIGNING
326                 * <li>has tokens MODIFY and WHERE
327                 * <li>has tokens MODIFY and REFERENCE INTO
328                 * <li>has tokens MODIFY and USING KEY
329                 * </ul>
330                 */
331                @Override
332                public boolean hasSureITabSyntax() {
333                        return TokenStreamUtils.startsWith(tokens, ETokenType.MODIFY, ETokenType.TABLE)
334                                        || TokenStreamUtils.containsAny(tokens, ETokenType.INDEX, ETokenType.TRANSPORTING,
335                                                        ETokenType.ASSIGNING, ETokenType.WHERE)
336                                        || TokenStreamUtils.containsSequence(tokens, 0, tokens.size() - 1, ETokenType.REFERENCE,
337                                                        ETokenType.INTO)
338                                        || TokenStreamUtils.containsSequence(tokens, 0, tokens.size() - 1, ETokenType.USING,
339                                                        ETokenType.KEY);
340                }
341        }
342
343        /** UPDATE */
344        private static class TableUpdate extends OpenSqlWriteStatement {
345
346                /** Constructor. */
347                protected TableUpdate(ShallowEntity entity, List<IToken> tokens, ITypeResolution typeResolution) {
348                        super(entity, tokens, typeResolution);
349                }
350
351                /** There is no update command for internal tables. */
352                @Override
353                public boolean hasSureITabSyntax() {
354                        return false;
355                }
356
357        }
358}