/*
* Copyright (C) 2014 Francis Galiegue <fgaliegue@gmail.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.
*/
package com.github.fge.grappa.matchers.join;
import com.github.fge.grappa.exceptions.GrappaException;
import com.github.fge.grappa.matchers.MatcherType;
import com.github.fge.grappa.matchers.base.CustomDefaultLabelMatcher;
import com.github.fge.grappa.matchers.base.Matcher;
import com.github.fge.grappa.rules.Rule;
import com.github.fge.grappa.run.context.MatcherContext;
/**
* A joining matcher
*
* <p>Such a matcher has two submatchers: a "joined" matcher and a "joining"
* matcher.</p>
*
* <p>This matcher will run cycles through both of these; the first will be a
* run of the "joined" matcher, subsequent cycles will be ("joining", "joined").
* </p>
*
* <p>Therefore:</p>
*
* <ul>
* <li>one cycle is {@code joined};</li>
* <li>two cycles is {@code joined, joining, joined};</li>
* <li>etc etc.</li>
* </ul>
*
* <p>This matcher will correctly reset the index to the last successful
* match; for instance, if the match sequence is {@code joined, joining, joined,
* joining} and two cycles are enough, it will reset the index to before the
* last {@code joining} so that subsequent matchers can proceed from there.</p>
*
* <p>It is <strong>forbidden</strong> for the "joining" matcher to match an
* empty sequence. Unfortunately, due to current limitations, this can only
* be detected at runtime.</p>
*
* <p>This matcher is not built directly; its build is initiated by a {@link
* JoinMatcherBootstrap}. Example:</p>
*
* <pre>
* Rule threeDigitsExactly()
* {
* return join(digit()).using('.').times(3);
* }
* </pre>
*
* @see JoinMatcherBootstrap
*/
public abstract class JoinMatcher
extends CustomDefaultLabelMatcher<JoinMatcher>
{
private static final int JOINED_CHILD_INDEX = 0;
private static final int JOINING_CHILD_INDEX = 1;
protected final Matcher joined;
protected final Matcher joining;
protected JoinMatcher(final Rule joined, final Rule joining)
{
super(new Rule[] { joined, joining }, "join");
this.joined = getChildren().get(JOINED_CHILD_INDEX);
this.joining = getChildren().get(JOINING_CHILD_INDEX);
}
@Override
public final MatcherType getType()
{
return MatcherType.COMPOSITE;
}
/**
* Tries a match on the given MatcherContext.
*
* @param context the MatcherContext
* @return true if the match was successful
*/
@Override
public final <V> boolean match(final MatcherContext<V> context)
{
/*
* TODO! Check logic
*
* At this point, if we have enough cycles, we can't determined whether
* our joining rule would match empty... Which is illegal.
*/
int cycles = 0;
if (!joined.getSubContext(context).runMatcher())
return enoughCycles(cycles);
cycles++;
Object snapshot = context.getValueStack().takeSnapshot();
int beforeCycle = context.getCurrentIndex();
while (runAgain(cycles) && matchCycle(context, beforeCycle)) {
beforeCycle = context.getCurrentIndex();
snapshot = context.getValueStack().takeSnapshot();
cycles++;
}
context.getValueStack().restoreSnapshot(snapshot);
context.setCurrentIndex(beforeCycle);
return enoughCycles(cycles);
}
protected abstract boolean runAgain(final int cycles);
protected abstract boolean enoughCycles(final int cycles);
protected final <V> boolean matchCycle(final MatcherContext<V> context,
final int beforeCycle)
{
if (!joining.getSubContext(context).runMatcher())
return false;
if (context.getCurrentIndex() == beforeCycle)
throw new GrappaException("joining rule (" + joining + ") of a "
+ "JoinMatcher cannot match an empty character sequence!");
return joined.getSubContext(context).runMatcher();
}
}