View Javadoc

1   /*
2    * Zemucan: A Syntax Assistant for DB2
3    * Copyright (C) 2009, 2010 Andres Gomez Casanova
4    *
5    * This file is part of Zemucan.
6    *
7    * Zemucan is free software: you can redistribute it and/or modify
8    * it under the terms of the GNU Lesser General Public License as published by
9    * the Free Software Foundation; either version 3 of the License, or
10   * (at your option) any later version.
11   *
12   * Zemucan is distributed in the hope that it will be useful,
13   * but WITHOUT ANY WARRANTY; without even the implied warranty of
14   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15   * GNU Lesser General Public License for more details.
16   *
17   * You should have received a copy of the GNU Lesser General Public License
18   * along with this library; if not, see <http://www.gnu.org/licenses/>.
19   *
20   * Contact:
21   * a n g o c a  at  y a h o o  dot  c o m
22   * Cra. 45 No 61 - 31, Bogota, Colombia.
23   *
24   * Author:   $LastChangedBy: angoca $:
25   * Date:     $LastChangedDate: 2011-03-06 22:24:44 -0500 (dom, 06 mar 2011) $:
26   * Revision: $LastChangedRevision: 1915 $:
27   * URL:      $HeadURL: https://zemucan.svn.sourceforge.net/svnroot/zemucan/branches/zemucan_v1/source-code/analyzers/src/main/java/name/angoca/zemucan/core/lexical/impl/ImplementationLexicalAnalyzer.java $:
28   */
29  package name.angoca.zemucan.core.lexical.impl;
30  
31  import java.util.ArrayList;
32  import java.util.List;
33  import java.util.StringTokenizer;
34  
35  import name.angoca.zemucan.AbstractZemucanException;
36  import name.angoca.zemucan.GeneralException;
37  import name.angoca.zemucan.ParameterNullException;
38  import name.angoca.zemucan.core.lexical.api.AbstractLexicalAnalyzer;
39  import name.angoca.zemucan.core.lexical.model.Token;
40  import name.angoca.zemucan.core.syntactic.impl.ImplementationSyntacticAnalyzer;
41  import name.angoca.zemucan.core.syntactic.model.GraphAnswer;
42  import name.angoca.zemucan.grammarReader.api.GrammarReaderController;
43  import name.angoca.zemucan.interfaze.model.ReturnOptions;
44  import name.angoca.zemucan.tools.Base64;
45  
46  import org.slf4j.Logger;
47  import org.slf4j.LoggerFactory;
48  
49  /**
50   * This is the implementation of the lexical analyzer. This class is not thread
51   * safe, because the singleton does not have synchronized.
52   * <p>
53   * <b>Control Version</b>
54   * <p>
55   * <ul>
56   * <li>0.0.1 Class creation.</li>
57   * <li>0.1.0</li>
58   * <li>0.2.0</li>
59   * <li>0.3.0 Recommendations from PMD.</li>
60   * <li>0.3.1 Organized.</li>
61   * <li>0.4.0 Invalid Graph Exception.</li>
62   * <li>0.5.0 Destroy instance.</li>
63   * <li>0.5.1 Logger messages.</li>
64   * <li>0.6.0 Delimiters not set in a static way.</li>
65   * <li>1.0.0 Moved to version 1.</li>
66   * <li>1.1.0 Exception hierarchy changed.</li>
67   * <li>1.1.1 Not synchronized, not thread safe.</li>
68   * <li>1.2.0 throws and asserts.</li>
69   * <li>1.2.1 Space after phrase.</li>
70   * <li>1.2.2 Delimiters from grammar controller.</li>
71   * <li>1.2.3 Synchronization.</li>
72   * <li>1.3.0 Method renamed.</li>
73   * <li>1.3.1 Clonable.</li>
74   * <li>1.3.2 Assistance fix.</li>
75   * <li>1.3.3 Ends with space.</li>
76   * <li>1.4.0 Method deleted and javadoc for new methods.</li>
77   * <li>1.4.1 Analysis improved, implementing 9 possible cases.</li>
78   * </ul>
79   *
80   * @author Andres Gomez Casanova <a
81   *         href="mailto:a n g o c a at y a h o o dot c o m" >(AngocA)</a>
82   * @version 1.4.1 2010-08-08
83   */
84  public final class ImplementationLexicalAnalyzer extends
85  		AbstractLexicalAnalyzer {
86  
87  	/**
88  	 * Huevo's code.
89  	 */
90  	private static byte[] CODE = new byte[] { 97, 110, 103, 111, 99, 97 };
91  	/**
92  	 * Huevo.
93  	 */
94  	private static String HUEVO = "VGhpcyBzb2Z0d2FyZSB3YXMgd3JpdHRlbiBieSBBbmRyZXMgR29tZXogQ2FzYW5vdmEgKEFuZ29jQSksIGFuZCBpdCBpcyBkZWRpY2F0ZWQgdG8gbXkgbG92ZWx5IExpbGlhbmEgT3JqdWVsYSwgd2hvIGlzIHRoZSBnaXJsIHRoYXQgSSBsb3ZlIGZyb20gdGhlIGRlZXAgb2YgbXkgaGVhcnQhIFRoaXMgYXBwbGljYXRpb24gaGFzIGEgaGlnaCBxdWFsaXR5IGluIHNldmVyYWwgYXNwZWN0cyBiZWNhdXNlIExpbGkgaXMgbXkgaW5zcGlyYXRpb24u";
95  	/**
96  	 * The only instance of this object.
97  	 */
98  	private static ImplementationLexicalAnalyzer instance;
99  
100 	/**
101 	 * Logger.
102 	 */
103 	private static final Logger LOGGER = LoggerFactory
104 			.getLogger(ImplementationLexicalAnalyzer.class);
105 
106 	/**
107 	 * Symbol that represents the separation of token with no meaning.
108 	 */
109 	private static final String WHITE_SPACE = " "; //$NON-NLS-1$
110 
111 	/**
112 	 * Destroys the instance. It is useful for testings purposes.
113 	 */
114 	public static void destroyInstance() {
115 		ImplementationLexicalAnalyzer.LOGGER
116 				.debug("Destroying ImplementationLexicalAnalyzer instance"); //$NON-NLS-1$
117 		if (ImplementationLexicalAnalyzer.instance != null) {
118 			ImplementationSyntacticAnalyzer.destroyInstance();
119 			ImplementationLexicalAnalyzer.instance.delimiters = null;
120 			ImplementationLexicalAnalyzer.instance = null;
121 		}
122 
123 		assert ImplementationLexicalAnalyzer.instance == null;
124 	}
125 
126 	/**
127 	 * Creates the only possible instance of this object. This method can be
128 	 * called after defining the delimiters for this object. It is not thread
129 	 * safe because there is not a synchronized part.
130 	 * <p>
131 	 * This method has a part where it is synchronized, however it is not thread
132 	 * safe because of the problem with the Single Pattern in Java
133 	 * (http://www.ibm.com/developerworks/java/library/j-dcl.html)
134 	 *
135 	 * @return This object instanced.
136 	 * @throws AbstractZemucanException
137 	 *             When there is a problem creating the graph or a null
138 	 *             parameter.
139 	 */
140 	public static ImplementationLexicalAnalyzer/* ! */getInstance()
141 			throws AbstractZemucanException {
142 		if (ImplementationLexicalAnalyzer.instance == null) {
143 			ImplementationLexicalAnalyzer.LOGGER
144 					.debug("Creating ImplementationLexicalAnalyzer instance"); //$NON-NLS-1$
145 			synchronized (ImplementationLexicalAnalyzer.class) {
146 				ImplementationLexicalAnalyzer.instance = new ImplementationLexicalAnalyzer();
147 			}
148 		}
149 
150 		assert ImplementationLexicalAnalyzer.instance != null;
151 		return ImplementationLexicalAnalyzer.instance;
152 	}
153 
154 	/**
155 	 * Set of delimiters.
156 	 */
157 	private String delimiters;
158 
159 	/**
160 	 * Constructor that defines the delimiters of the tokens.
161 	 *
162 	 * @throws AbstractZemucanException
163 	 *             If there is a null parameter. When there is a problem when
164 	 *             creating the graph or a null parameter.
165 	 */
166 	private ImplementationLexicalAnalyzer() throws AbstractZemucanException {
167 		super();
168 		// This helps to run the processPhrase method faster the first
169 		// time, because the grammar has to be read.
170 		this.delimiters = GrammarReaderController.getInstance().getDelimiters();
171 	}
172 
173 	/*
174 	 * (non-Javadoc)
175 	 *
176 	 * @see
177 	 * name.angoca.zemucan.core.lexical.api.AbstractLexicalAnalyzer#analyzePhrase
178 	 * (java.lang.String)
179 	 */
180 	@Override
181 	public ReturnOptions/* ? */analyzePhrase(final String/* ! */phrase)
182 			throws AbstractZemucanException {
183 		assert phrase != null;
184 
185 		// TODO v1.1 reducir el cyclomatic complexity.
186 		if (ImplementationLexicalAnalyzer.LOGGER.isDebugEnabled()) {
187 			ImplementationLexicalAnalyzer.LOGGER.debug(
188 					"Entering to lexical analyzer: '{}'", //$NON-NLS-1$
189 					phrase);
190 		}
191 
192 		// Delete all the unnecessary spaces.
193 		final String newPhrase = phrase.trim();
194 		final boolean endsWithSpace = phrase
195 				.endsWith(ImplementationLexicalAnalyzer.WHITE_SPACE);
196 
197 		final StringTokenizer tokenizer = new StringTokenizer(newPhrase,
198 				this.delimiters, true);
199 		final ReturnOptions ret1 = this.pascua(phrase);
200 
201 		final List<Token> tokens = new ArrayList<Token>(tokenizer.countTokens());
202 		String currentToken;
203 		// Scan the tokens and gets only the non space tokens.
204 		while (tokenizer.hasMoreTokens()) {
205 			currentToken = tokenizer.nextToken();
206 			if (!(currentToken
207 					.equals(ImplementationLexicalAnalyzer.WHITE_SPACE))) {
208 				// FIXME v1.1 por que true? probablemente va tocar
209 				// crear un token de entrada que es sencillo, y otro de
210 				// salida que sera mas complejo y que tendra mas
211 				// informacion. En este caso se usaria el sencillo.
212 				tokens.add(new Token(currentToken, true));
213 				ImplementationLexicalAnalyzer.LOGGER.debug(
214 						"Token: {{}}", currentToken); //$NON-NLS-1$
215 			}
216 		}
217 
218 		final GraphAnswer answer = ImplementationSyntacticAnalyzer
219 				.getInstance().analyzeTokens(tokens, endsWithSpace);
220 
221 		if (ImplementationLexicalAnalyzer.LOGGER.isDebugEnabled()) {
222 			ImplementationLexicalAnalyzer.LOGGER.debug("{} : {}", //$NON-NLS-1$
223 					answer.getPhrases().toString(), answer.getOptions()
224 							.toString());
225 		}
226 
227 		// Process the syntax answer to create a set of options for
228 		// the UI.
229 		ReturnOptions ret = this.processAnswer(newPhrase, endsWithSpace,
230 				tokens, answer);
231 		if (ret1 != null) {
232 			ret = ret1;
233 		}
234 
235 		assert ret != null;
236 
237 		return ret;
238 	}
239 
240 	/**
241 	 * Huevo de pascua.
242 	 *
243 	 * @param phrase
244 	 *            User's phrase.
245 	 * @throws ParameterNullException
246 	 *             Never.
247 	 */
248 	private ReturnOptions/* ? */pascua(final String/* ! */phrase)
249 			throws ParameterNullException {
250 		assert phrase != null;
251 
252 		ReturnOptions ret = null;
253 		if (phrase.equals(new String(ImplementationLexicalAnalyzer.CODE))) {
254 			final String decode = Base64
255 					.decodeString(ImplementationLexicalAnalyzer.HUEVO);
256 			ret = new ReturnOptions(phrase, new String[] {},
257 					new String[] { decode });
258 		}
259 
260 		return ret;
261 	}
262 
263 	private ReturnOptions phraseMultiple(String newPhrase,
264 			final List<Token> tokens, final GraphAnswer answer,
265 			final int sizeOptions, String[] setOfOptions, String[] setOfPhrases)
266 			throws GeneralException, ParameterNullException {
267 		if (sizeOptions > 1) {
268 			// There are multiples options and at multiple phrases, then show
269 			// all of them.
270 			ImplementationLexicalAnalyzer.LOGGER.debug("Case 1!");
271 		} else if (sizeOptions == 1) {
272 			// There are just one option and multiple phrases, then show all of
273 			// them.
274 			ImplementationLexicalAnalyzer.LOGGER.debug("Case 2!");
275 			// TODO v1.1 this is just for to know this unknown case.
276 			throw new RuntimeException("Notify this to AngocA, case 2: "
277 					+ newPhrase);
278 		} else if (sizeOptions == 0) {
279 			// There are multiple phrases and 0 options.
280 			ImplementationLexicalAnalyzer.LOGGER.debug("Case 3!");
281 			final String prefix = this
282 					.samePrefixForPhrases(answer.getPhrases());
283 			if (prefix.length() > 0) {
284 				// All phrases have the same prefix, then replace last token.
285 				newPhrase = this.replaceLastToken(newPhrase, tokens, prefix);
286 			}
287 		}
288 		setOfOptions = this.returnCandidates(answer.getOptions());
289 		setOfPhrases = this.returnCandidates(answer.getPhrases());
290 		final ReturnOptions ret = new ReturnOptions(newPhrase, setOfPhrases,
291 				setOfOptions);
292 		return ret;
293 	}
294 
295 	private ReturnOptions phraseOne(String newPhrase, final List<Token> tokens,
296 			final GraphAnswer answer, final int sizeOptions,
297 			String[] setOfOptions, String[] setOfPhrases)
298 			throws GeneralException, ParameterNullException {
299 		if (sizeOptions > 1) {
300 			// There are one phrase and multiple options, then add them.
301 			ImplementationLexicalAnalyzer.LOGGER.debug("Case 4!");
302 			setOfPhrases = this.returnCandidates(answer.getPhrases());
303 		} else if (sizeOptions == 1) {
304 			// There are one phrase and one option, then add them.
305 			ImplementationLexicalAnalyzer.LOGGER.debug("Case 5!");
306 			setOfPhrases = this.returnCandidates(answer.getPhrases());
307 		} else if (sizeOptions == 0) {
308 			// There is just one phrase, then complete the command.
309 			ImplementationLexicalAnalyzer.LOGGER.debug("Case 6!");
310 			// Replace the last token with the phrase.
311 			newPhrase = this.replaceLastToken(newPhrase, tokens, answer
312 					.getPhrases().get(0).getToken());
313 			setOfPhrases = new String[] {};
314 		}
315 		setOfOptions = this.returnCandidates(answer.getOptions());
316 		final ReturnOptions ret = new ReturnOptions(newPhrase, setOfPhrases,
317 				setOfOptions);
318 		return ret;
319 	}
320 
321 	private ReturnOptions phraseZero(String newPhrase,
322 			final boolean endsWithSpace, final GraphAnswer answer,
323 			final int sizeOptions, String[] setOfOptions, String[] setOfPhrases)
324 			throws GeneralException, ParameterNullException {
325 		if (sizeOptions > 1) {
326 			// There are multiple options and 0 phrases, then show all options
327 			// and add a space at the end.
328 			ImplementationLexicalAnalyzer.LOGGER.debug("Case 7");
329 			if (newPhrase.length() > 0) {
330 				newPhrase += " ";
331 			}
332 			newPhrase += this.samePrefixForPhrases(answer.getOptions());
333 			setOfOptions = this.returnCandidates(answer.getOptions());
334 		} else if (sizeOptions == 1) {
335 			// There is just one option.
336 			ImplementationLexicalAnalyzer.LOGGER.debug("Case 8!");
337 			newPhrase += " ";
338 			if (answer.getOptions().get(0).isReservedWord()) {
339 				// The option is a reserved word, then concatenate the
340 				// option.
341 				ImplementationLexicalAnalyzer.LOGGER.debug("Case 8a");
342 				newPhrase += answer.getOptions().get(0).getToken();
343 				setOfOptions = new String[] {};
344 			} else {
345 				// The option is not a reserved word, then show option.
346 				ImplementationLexicalAnalyzer.LOGGER.debug("Case 8b");
347 				setOfOptions = this.returnCandidates(answer.getOptions());
348 			}
349 		} else if (sizeOptions == 0) {
350 			// There are not options nor phrases. This is due to a valid
351 			// command or an unknown or miswritten command.
352 			ImplementationLexicalAnalyzer.LOGGER.debug("Case 9!");
353 			// If the unknown command has a trailing space, it conserves it.
354 			if (endsWithSpace) {
355 				newPhrase += " ";
356 			}
357 			// The arrays are empty, no assistance.
358 			setOfOptions = new String[] {};
359 		}
360 		setOfPhrases = new String[] {};
361 		final ReturnOptions ret = new ReturnOptions(newPhrase, setOfPhrases,
362 				setOfOptions);
363 		return ret;
364 	}
365 
366 	/**
367 	 * Process the answer of the syntactic analyzer and creates the arrays for
368 	 * the UI.
369 	 *
370 	 * @param newPhrase
371 	 *            User's phrase converted.
372 	 * @param endsWithSpace
373 	 *            If the phrase finishes with an empty space.
374 	 * @param tokens
375 	 *            List of tokens that represent the phrase.
376 	 * @param answer
377 	 *            Answer of the syntactic analyzer.
378 	 * @return List of candidates for the UI.
379 	 * @throws AbstractZemucanException
380 	 *             If there is a problem retrieving the options, phrases or
381 	 *             creating the arrays.
382 	 */
383 	private ReturnOptions processAnswer(final String newPhrase,
384 			final boolean endsWithSpace, final List<Token> tokens,
385 			final GraphAnswer answer) throws AbstractZemucanException {
386 		final int sizePhrases = answer.getPhrases().size();
387 		final int sizeOptions = answer.getOptions().size();
388 		final String[] setOfOptions = null;
389 		final String[] setOfPhrases = null;
390 		ReturnOptions ret = null;
391 		if (sizePhrases > 1) {
392 			ret = this.phraseMultiple(newPhrase, tokens, answer, sizeOptions,
393 					setOfOptions, setOfPhrases);
394 		} else if (sizePhrases == 1) {
395 			ret = this.phraseOne(newPhrase, tokens, answer, sizeOptions,
396 					setOfOptions, setOfPhrases);
397 		} else if (sizePhrases == 0) {
398 			ret = this.phraseZero(newPhrase, endsWithSpace, answer,
399 					sizeOptions, setOfOptions, setOfPhrases);
400 		}
401 
402 		return ret;
403 	}
404 
405 	/**
406 	 * Replaces the last token of the typed command.
407 	 *
408 	 * @param phrase
409 	 *            Current phrase.
410 	 * @param tokens
411 	 *            List of tokens that compose the phrase.
412 	 * @param prefix
413 	 *            Common prefix to the candidates.
414 	 * @return New phrase.
415 	 */
416 	String/* ! */replaceLastToken(final String/* ! */phrase,
417 			final List<Token>/* <!>! */tokens, final String/* ! */prefix) {
418 		assert phrase != null;
419 		assert tokens != null;
420 		boolean assertsEnabled = false;
421 		// Intentional side-effect!
422 		assert assertsEnabled = true;
423 		if (assertsEnabled) {
424 			for (final Token token : tokens) {
425 				assert token != null;
426 			}
427 		}
428 		assert prefix != null;
429 
430 		final int lastIndexPhrase = tokens.size() - 1;
431 		// Retrieves the last token of the given phrase.
432 		final String lastTokenPhrase = tokens.get(lastIndexPhrase).getToken();
433 		// Search the beginning of last occurrence of the last token.
434 		final int lastTokenIndex = phrase.lastIndexOf(lastTokenPhrase);
435 		String newPhrase = "";
436 		if (lastIndexPhrase != 0) {
437 			// Creates a new phrase without the last token.
438 			newPhrase = phrase.substring(0, lastTokenIndex) + prefix;
439 		} else {
440 			newPhrase = prefix;
441 		}
442 
443 		assert newPhrase != null;
444 
445 		return newPhrase;
446 	}
447 
448 	/**
449 	 * Converts the candidate's list in an array.
450 	 *
451 	 * @param candidates
452 	 *            List of candidates.
453 	 * @return Array of candidates.
454 	 */
455 	private String[]/* [!]! */returnCandidates(
456 			final List<Token>/* <!>! */candidates) {
457 		assert candidates != null;
458 		boolean assertsEnabled = false;
459 		// Intentional side-effect!
460 		assert assertsEnabled = true;
461 		if (assertsEnabled) {
462 			for (final Token token : candidates) {
463 				assert token != null;
464 			}
465 		}
466 
467 		final int size = candidates.size();
468 		final String[] ret = new String[size];
469 		for (int i = 0; i < size; i += 1) {
470 			final Token token = candidates.get(i);
471 			ret[i] = token.getToken();
472 		}
473 
474 		assert ret != null;
475 		// Intentional side-effect!
476 		assert assertsEnabled = true;
477 		if (assertsEnabled) {
478 			for (int i = 0; i < ret.length; i++) {
479 				assert ret[i] != null;
480 			}
481 		}
482 
483 		return ret;
484 	}
485 
486 	/**
487 	 * Calculates the prefix of a set of candidates. That means that it returns
488 	 * the common part of a set of
489 	 *
490 	 * @param tokens
491 	 *            List of phrase candidates.
492 	 * @return Prefix. Empty if there are not common part between phrases.
493 	 */
494 	private String/* ! */samePrefixForPhrases(final List<Token> /* ! */tokens) {
495 		assert tokens != null;
496 		boolean assertsEnabled = false;
497 		// Intentional side-effect!
498 		assert assertsEnabled = true;
499 		if (assertsEnabled) {
500 			for (final Token token : tokens) {
501 				assert token != null;
502 			}
503 		}
504 
505 		final String example = tokens.get(0).getToken();
506 		final int sizePrefix = example.length();
507 		String prefix = "";
508 		boolean allSamePrefix = true;
509 		for (int i = 0; (i < sizePrefix) && allSamePrefix; i += 1) {
510 			final String temp = example.substring(0, i + 1);
511 			// Checks all phrases against the example.
512 			for (final Token token : tokens) {
513 				allSamePrefix &= token.getToken().startsWith(temp);
514 			}
515 			// All phrases starts with the same prefix.
516 			if (allSamePrefix) {
517 				prefix = temp;
518 			}
519 		}
520 
521 		assert prefix != null;
522 
523 		return prefix;
524 	}
525 }