/*
* Copyright (c) 2011 Petter Holmström
*
* 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.peholmst.mvp4vaadin.navigation;
import java.util.Collections;
import java.util.EmptyStackException;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Stack;
import com.github.peholmst.mvp4vaadin.View;
import com.github.peholmst.mvp4vaadin.navigation.events.CurrentNavigationControllerViewChangedEvent;
import com.github.peholmst.mvp4vaadin.navigation.events.ViewAttachedToNavigationControllerEvent;
import com.github.peholmst.mvp4vaadin.navigation.events.ViewDetachedFromNavigationControllerEvent;
import com.github.peholmst.stuff4vaadin.visitor.VisitableList;
import com.github.peholmst.stuff4vaadin.visitor.Visitor;
/**
* This is the default implementation of the {@link NavigationController}
* interface. Create a new instance using the default constructor and attach
* views using the {@link #navigate(NavigationRequest)} method. You can use the
* {@link NavigationRequestBuilder} to create {@link NavigationRequest}s.
*
* @author Petter Holmström
* @since 1.0
*/
public class DefaultNavigationController implements NavigationController {
private static final long serialVersionUID = 6838003395877804584L;
private final Stack<View> viewStack = new Stack<View>();
private final VisitableList<NavigationControllerListener> listeners = new VisitableList<NavigationControllerListener>();
@Override
public NavigationResult navigate(NavigationRequest request) {
final View fromView = getCurrentView();
final int differenceIndex = getIndexOfFirstDifferenceFromStack(request);
if (differenceIndex == viewStack.size()) {
// We're attaching new stacks to the view
attachRemainingViewsInRequest(request);
if (fromView != null) {
invokeNavigatedFromViewOnView(fromView);
}
} else {
// We have to detach some views (including the current view) before
// we can attach new views
final NavigationResult result = detachViewsFromStack(differenceIndex);
if (result.equals(NavigationResult.SUCCEEDED)) {
attachRemainingViewsInRequest(request);
} else {
if (result.equals(NavigationResult.INTERRUPTED)) {
fireEvent(new CurrentNavigationControllerViewChangedEvent(
this, fromView, getCurrentView()));
}
return result;
}
}
invokeNavigatedToViewOnCurrentView(request.getParams(), fromView);
fireEvent(new CurrentNavigationControllerViewChangedEvent(this,
fromView, getCurrentView()));
return NavigationResult.SUCCEEDED;
}
/**
* Compares the request path to the view stack. The returned value is the
* index of the first element that differs between these two.
*/
private int getIndexOfFirstDifferenceFromStack(NavigationRequest request) {
final List<View> path = request.getPath();
for (int i = 0; i < viewStack.size(); ++i) {
final View viewInStack = viewStack.get(i);
if (i < path.size()) {
final View viewInPath = path.get(i);
if (!viewInStack.equals(viewInPath)) {
return i;
}
} else {
return i;
}
}
return viewStack.size();
}
/**
* If the request path contains more elements than the view stack, the
* remaining views from the path are added to the stack. No comparison of
* the stack and the request path is made.
*/
private void attachRemainingViewsInRequest(NavigationRequest request) {
for (int i = viewStack.size(); i < request.getPath().size(); ++i) {
final View viewInPath = request.getPath().get(i);
attach(viewInPath);
}
}
/**
* Adds the view to the view stack.
*/
private void attach(View view) {
viewStack.add(view);
if (view.supportsAdapter(NavigationControllerCallback.class)) {
view.adapt(NavigationControllerCallback.class)
.attachedToController(this);
}
fireEvent(new ViewAttachedToNavigationControllerEvent(this, view));
}
/**
* Detaches all the views from the start, starting from the top-most view
* and going downwards until the view at
* <code>indexOfFinalViewToDetach</code> has been detached.
*
* @return {@link NavigationResult#PREVENTED} if the top-most view aborted
* the operation, {@link NavigationResult#INTERRUPTED} if any of the
* other views aborted the operation, or
* {@link NavigationResult#SUCCEEDED} if all the views were
* detached.
*/
private NavigationResult detachViewsFromStack(int indexOfFinalViewToDetach) {
boolean currentViewRemoved = false;
while (viewStack.size() > indexOfFinalViewToDetach) {
if (!detachTopmostView()) {
if (currentViewRemoved) {
return NavigationResult.INTERRUPTED;
} else {
return NavigationResult.PREVENTED;
}
}
currentViewRemoved = true;
}
return NavigationResult.SUCCEEDED;
}
/**
* Attempts to remove the top-most view from the stack. Returns true on
* success and false on failure.
*/
private boolean detachTopmostView() {
final View view = viewStack.peek();
if (view.supportsAdapter(NavigationControllerCallback.class)) {
if (!view.adapt(NavigationControllerCallback.class)
.detachingFromController(this)) {
return false;
}
viewStack.pop();
view.adapt(NavigationControllerCallback.class)
.detachedFromController(this);
} else {
viewStack.pop();
}
fireEvent(new ViewDetachedFromNavigationControllerEvent(this, view));
return true;
}
private void invokeNavigatedToViewOnCurrentView(Map<String, Object> params,
View fromView) {
if (!viewStack.isEmpty()) {
if (getCurrentView().supportsAdapter(
NavigationControllerCallback.class)) {
getCurrentView().adapt(NavigationControllerCallback.class)
.navigatedToView(params, fromView);
}
}
}
private void invokeNavigatedFromViewOnView(View fromView) {
if (fromView.supportsAdapter(NavigationControllerCallback.class)) {
fromView.adapt(NavigationControllerCallback.class)
.navigatedFromView(getCurrentView());
}
}
@Override
public boolean navigateBack() {
if (isEmpty()) {
return false;
} else if (viewStack.size() == 1) {
return clear() == NavigationResult.SUCCEEDED;
} else {
final NavigationRequest request = NavigationRequestBuilder
.newInstance().startWithPathToPreviousView(this)
.buildRequest();
return navigate(request) == NavigationResult.SUCCEEDED;
}
}
@Override
public List<View> getViewStack() {
return Collections.unmodifiableList(viewStack);
}
/**
* This method is intended for unit testing only! Do not use for anything
* else!
*/
Stack<View> getModifiableViewStack() {
return viewStack;
}
@Override
public View getCurrentView() {
try {
return viewStack.peek();
} catch (EmptyStackException e) {
return null;
}
}
@Override
public View getFirstView() {
try {
return viewStack.firstElement();
} catch (NoSuchElementException e) {
return null;
}
}
@Override
public boolean isEmpty() {
return viewStack.isEmpty();
}
@Override
public boolean containsMoreThanOneElement() {
return viewStack.size() > 1;
}
@Override
public NavigationResult clear() {
final View oldView = getCurrentView();
final NavigationResult result = detachViewsFromStack(0);
if (getCurrentView() != oldView) {
fireEvent(new CurrentNavigationControllerViewChangedEvent(this,
oldView, getCurrentView()));
}
return result;
}
/**
* Notifies all registered listeners of the specified event.
*/
protected void fireEvent(final NavigationControllerEvent event) {
listeners.visitItems(new Visitor<NavigationControllerListener>() {
@Override
public void visit(NavigationControllerListener visitable) {
visitable.handleNavigationControllerEvent(event);
}
});
}
@Override
public void addListener(NavigationControllerListener listener) {
if (listener != null) {
listeners.add(listener);
}
}
@Override
public void removeListener(NavigationControllerListener listener) {
if (listener != null) {
listeners.remove(listener);
}
}
}