| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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 org.netbeans.modules.languages.yaml; |
| |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import javax.swing.event.ChangeListener; |
| import org.netbeans.api.lexer.Token; |
| import org.netbeans.api.lexer.TokenHierarchy; |
| import org.netbeans.api.lexer.TokenId; |
| import org.netbeans.api.lexer.TokenSequence; |
| import org.netbeans.modules.csl.api.Severity; |
| import org.netbeans.modules.csl.api.StructureItem; |
| import org.netbeans.modules.csl.spi.DefaultError; |
| import org.netbeans.modules.parsing.api.Snapshot; |
| import org.netbeans.modules.parsing.api.Task; |
| import org.netbeans.modules.parsing.spi.ParseException; |
| import org.netbeans.modules.parsing.spi.SourceModificationEvent; |
| import org.openide.util.NbBundle; |
| import org.snakeyaml.engine.v2.exceptions.ParserException; |
| import org.snakeyaml.engine.v2.exceptions.ScannerException; |
| |
| /** |
| * Parser for YAML. Delegates to the YAML parser shipped with JRuby (jvyamlb) |
| * |
| * @author Tor Norbye |
| */ |
| public class YamlParser extends org.netbeans.modules.parsing.spi.Parser { |
| |
| private static final Logger LOGGER = Logger.getLogger(YamlParser.class.getName()); |
| /** |
| * The max length for files we will try to parse (to avoid OOMEs). |
| */ |
| private static final int MAX_LENGTH = 512 * 1024; |
| private YamlParserResult lastResult; |
| |
| @Override |
| public void addChangeListener(ChangeListener changeListener) { |
| // FIXME parsing API |
| } |
| |
| @Override |
| public void removeChangeListener(ChangeListener changeListener) { |
| // FIXME parsing API |
| } |
| |
| @Override |
| public void cancel() { |
| // FIXME parsing API |
| } |
| |
| @Override |
| public Result getResult(Task task) throws ParseException { |
| assert lastResult != null : "getResult() called prior parse()"; //NOI18N |
| return lastResult; |
| } |
| |
| private static String asString(CharSequence sequence) { |
| if (sequence instanceof String) { |
| return (String) sequence; |
| } else { |
| return sequence.toString(); |
| } |
| } |
| |
| private boolean isTooLarge(String source) { |
| return source.length() > MAX_LENGTH; |
| } |
| |
| private YamlParserResult resultForTooLargeFile(Snapshot snapshot) { |
| YamlParserResult result = new YamlParserResult(snapshot); |
| // FIXME this can violate contract of DefaultError (null fo) |
| DefaultError error = new DefaultError(null, NbBundle.getMessage(YamlParser.class, "TooLarge"), null, |
| snapshot.getSource().getFileObject(), 0, 0, Severity.WARNING); |
| result.addError(error); |
| return result; |
| } |
| |
| // package private for unit tests |
| static void replaceWithSpaces(StringBuilder source, String startToken, String endToken) { |
| int startReplace = source.indexOf(startToken); |
| if (startReplace == -1) { |
| return; |
| } |
| while (startReplace > -1) { |
| int endReplace = source.indexOf(endToken, startReplace) + 1; |
| if (endReplace > startReplace) { |
| for (int i = startReplace; i <= endReplace; i++) { |
| source.setCharAt(i, ' '); |
| } |
| startReplace = source.indexOf(startToken, endReplace); |
| } else { |
| startReplace = -1; |
| } |
| } |
| } |
| |
| static void replacePhpFragments(StringBuilder source) { |
| // this is a hack. The right solution would be to create a toplevel language, which |
| // will have embeded yaml and php. |
| // This code replaces php fragments with space, because jruby parser fails |
| // on this. |
| replaceWithSpaces(source, "<?", "?>"); |
| } |
| |
| static void replaceMustache(StringBuilder source) { |
| // this is a hack. The right solution would be to create a toplevel language, which |
| // will have embeded yaml and something. |
| // This code replaces mouthstache fragments with space. |
| replaceWithSpaces(source, "{{", "}}"); |
| } |
| |
| static final Pattern[] SPEC_PATTERN_REPLACEMENTS = new Pattern[]{ |
| Pattern.compile("@"), |
| Pattern.compile("\\?"), |
| Pattern.compile("!(?!(omap|!omap))"),}; |
| |
| private static void replaceCommonSpecialCharacters(StringBuilder source) { |
| |
| for (int i = 0; i < SPEC_PATTERN_REPLACEMENTS.length; i++) { |
| Pattern pattern = SPEC_PATTERN_REPLACEMENTS[i]; |
| Matcher m = pattern.matcher(source); |
| while (m.find()) { |
| for (int idx = m.start(); idx < m.end(); idx++) { |
| source.setCharAt(idx, '_'); |
| } |
| } |
| } |
| } |
| |
| private static String replaceInlineRegexBrackets(String source) { |
| // XXX this is just a workaround for issue #246787 |
| // it is caused by jvyamlb yaml parser |
| Pattern p = Pattern.compile("\\^/.*?\\[.*?\\].*?,"); |
| Matcher m = p.matcher(source); |
| while (m.find()) { |
| String found = m.group(); |
| String replaced = found.replace('[', '_').replace(']', '_'); |
| source = source.replace(found, replaced); |
| } |
| return source; |
| } |
| |
| // for test package private |
| YamlParserResult parse(String src, Snapshot snapshot) { |
| |
| if (isTooLarge(src)) { |
| return resultForTooLargeFile(snapshot); |
| } |
| |
| StringBuilder sb = new StringBuilder(src); |
| replacePhpFragments(sb); |
| replaceMustache(sb); |
| replaceCommonSpecialCharacters(sb); |
| // source = replaceInlineRegexBrackets(source); |
| |
| YamlParserResult result = new YamlParserResult(snapshot); |
| |
| LinkedList<YamlSection> sources = new LinkedList<>(); |
| sources.push(new YamlSection(sb.toString())); |
| int sourceLength = Integer.MAX_VALUE; |
| int stallCounter = 0; |
| while (!sources.isEmpty()) { |
| int len = 0; |
| for (YamlSection source : sources) { |
| len += source.length(); |
| } |
| |
| if (len < sourceLength) { |
| sourceLength = len; |
| stallCounter = 0; |
| } else { |
| stallCounter++; |
| } |
| if (stallCounter < 2) { |
| YamlSection section = sources.pop(); |
| try { |
| List<? extends StructureItem> items = section.collectItems(snapshot); |
| result.addStructure(items); |
| } catch (ScannerException se) { |
| result.addError(section.processException(snapshot, se)); |
| for (YamlSection part : section.splitOnException(se)) { |
| sources.push(part); |
| } |
| } catch (ParserException pe ){ |
| result.addError(section.processException(snapshot, pe)); |
| for (YamlSection part : section.splitOnException(pe)) { |
| sources.push(part); |
| } |
| } catch (Exception ex) { |
| String message = ex.getMessage(); |
| if (message != null && message.length() > 0) { |
| result.addError(processError(message, snapshot, 0)); |
| } |
| } |
| } else { |
| sources.clear(); |
| } |
| } |
| return result; |
| } |
| |
| private DefaultError processError(String message, Snapshot snapshot, int pos) { |
| // Strip off useless prefixes to make errors more readable |
| if (message.startsWith("ScannerException null ")) { // NOI18N |
| message = message.substring(22); |
| } else if (message.startsWith("ParserException ")) { // NOI18N |
| message = message.substring(16); |
| } |
| // Capitalize sentences |
| char firstChar = message.charAt(0); |
| char upcasedChar = Character.toUpperCase(firstChar); |
| if (firstChar != upcasedChar) { |
| message = upcasedChar + message.substring(1); |
| } |
| // FIXME this can violate contract of DefaultError (null fo) |
| return new DefaultError(null, message, null, snapshot.getSource().getFileObject(), |
| pos, pos, Severity.ERROR); |
| } |
| |
| @Override |
| public void parse(Snapshot snapshot, Task task, SourceModificationEvent event) throws ParseException { |
| |
| String source = asString(snapshot.getText()); |
| if (isTooLarge(source)) { |
| LOGGER.log(Level.FINE, |
| "Skipping {0}, too large to parse (length: {1})", |
| new Object[]{snapshot.getSource().getFileObject(), source.length()}); |
| lastResult = resultForTooLargeFile(snapshot); |
| return; |
| } |
| |
| try { |
| // int caretOffset = reader.getCaretOffset(file); |
| // if (caretOffset != -1 && job.translatedSource != null) { |
| // caretOffset = job.translatedSource.getAstOffset(caretOffset); |
| // } |
| |
| // Construct source by removing <% %> tokens etc. |
| StringBuilder sb = new StringBuilder(); |
| TokenHierarchy hi = TokenHierarchy.create(source, YamlTokenId.language()); |
| |
| TokenSequence ts = hi.tokenSequence(); |
| |
| // If necessary move ts to the requested offset |
| int offset = 0; |
| ts.move(offset); |
| |
| // int adjustedOffset = 0; |
| // int adjustedCaretOffset = -1; |
| while (ts.moveNext()) { |
| Token t = ts.token(); |
| TokenId id = t.id(); |
| |
| if (id == YamlTokenId.RUBY_EXPR) { |
| String marker = "__"; // NOI18N |
| // Marker |
| sb.append(marker); |
| // Replace with spaces to preserve offsets |
| for (int i = 0, n = t.length() - marker.length(); i < n; i++) { // -2: account for the __ |
| sb.append(' '); |
| } |
| } else if (id == YamlTokenId.RUBY || id == YamlTokenId.RUBYCOMMENT || id == YamlTokenId.DELIMITER) { |
| // Replace with spaces to preserve offsets |
| for (int i = 0; i < t.length(); i++) { |
| sb.append(' '); |
| } |
| } else { |
| sb.append(t.text().toString()); |
| } |
| |
| // adjustedOffset += t.length(); |
| } |
| |
| source = sb.toString(); |
| |
| lastResult = parse(source, snapshot); |
| } catch (Exception ioe) { |
| lastResult = new YamlParserResult(snapshot); |
| } |
| } |
| } |