001/* 002 * Copyright (C) 2009-2011 Mathias Doenitz 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.parboiled.errors; 018 019import org.parboiled.common.Formatter; 020import org.parboiled.common.StringUtils; 021import org.parboiled.matchers.AnyOfMatcher; 022import org.parboiled.matchers.Matcher; 023import org.parboiled.support.Chars; 024import org.parboiled.support.MatcherPath; 025import org.parboiled.common.Formatter; 026import org.parboiled.common.StringUtils; 027import org.parboiled.matchers.AnyOfMatcher; 028import org.parboiled.matchers.Matcher; 029import org.parboiled.support.Chars; 030import org.parboiled.support.MatcherPath; 031 032import java.util.ArrayList; 033import java.util.List; 034 035/** 036 * A {@link Formatter} for {@link InvalidInputError}s that automatically creates the correct "expected" text 037 * for the error. 038 */ 039public class DefaultInvalidInputErrorFormatter implements Formatter<InvalidInputError> { 040 041 public String format(InvalidInputError error) { 042 if (error == null) return ""; 043 044 int len = error.getEndIndex() - error.getStartIndex(); 045 StringBuilder sb = new StringBuilder(); 046 if (len > 0) { 047 char c = error.getInputBuffer().charAt(error.getStartIndex()); 048 if (c == Chars.EOI) { 049 sb.append("Unexpected end of input"); 050 } else { 051 sb.append("Invalid input '") 052 .append(StringUtils.escape(String.valueOf(c))); 053 if (len > 1) sb.append("..."); 054 sb.append('\''); 055 } 056 } else { 057 sb.append("Invalid input"); 058 } 059 String expectedString = getExpectedString(error); 060 if (StringUtils.isNotEmpty(expectedString)) { 061 sb.append(", expected ").append(expectedString); 062 } 063 return sb.toString(); 064 } 065 066 public String getExpectedString(InvalidInputError error) { 067 // In non recovery-mode there is no complexity in the error and start indices since they are all stable. 068 // However, in recovery-mode the RecoveringParseRunner inserts characters into the InputBuffer, which requires 069 // for all indices taken before to be shifted. The RecoveringParseRunner does this by changing the indexDelta 070 // of the parse runner. All users of the ParseError will then automatically see shifted start and end indices 071 // matching the state of the underlying InputBuffer. However, since the failed MatcherPaths still carry the 072 // "original" indices we need to unapply the IndexDelta in order to be able to compare with them. 073 int pathStartIndex = error.getStartIndex() - error.getIndexDelta(); 074 075 List<String> labelList = new ArrayList<String>(); 076 for (MatcherPath path : error.getFailedMatchers()) { 077 Matcher labelMatcher = ErrorUtils.findProperLabelMatcher(path, pathStartIndex); 078 if (labelMatcher == null) continue; 079 String[] labels = getLabels(labelMatcher); 080 for (String label : labels) { 081 if (label != null && !labelList.contains(label)) { 082 labelList.add(label); 083 } 084 } 085 } 086 return join(labelList); 087 } 088 089 /** 090 * Gets the labels corresponding to the given matcher, AnyOfMatchers are treated specially in that their 091 * label is constructed as a list of their contents 092 * 093 * @param matcher the matcher 094 * @return the labels 095 */ 096 public String[] getLabels(Matcher matcher) { 097 if ((matcher instanceof AnyOfMatcher) && ((AnyOfMatcher)matcher).characters.toString().equals(matcher.getLabel())) { 098 AnyOfMatcher cMatcher = (AnyOfMatcher) matcher; 099 if (!cMatcher.characters.isSubtractive()) { 100 String[] labels = new String[cMatcher.characters.getChars().length]; 101 for (int i = 0; i < labels.length; i++) { 102 labels[i] = '\'' + String.valueOf(cMatcher.characters.getChars()[i]) + '\''; 103 } 104 return labels; 105 } 106 } 107 return new String[] {matcher.getLabel()}; 108 } 109 110 public String join(List<String> labelList) { 111 StringBuilder sb = new StringBuilder(); 112 for (int i = 0; i < labelList.size(); i++) { 113 if (i > 0) sb.append(i < labelList.size() - 1 ? ", " : " or "); 114 sb.append(labelList.get(i)); 115 } 116 return StringUtils.escape(sb.toString()); 117 } 118 119}