/*
*
* * Copyright (c) 2016. David Sowerby
* *
* * 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 uk.q3c.krail.core.navigate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.inject.Inject;
import org.slf4j.Logger;
import javax.annotation.Nonnull;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Represents a navigation state (basically a URI fragment, potentially with parameters) but with its component parts
* already broken out by an implementation of {@link URIFragmentHandler}. It is used to remove the need to repeatedly
* break down the URI to get to parts of its content, but is entirely passive.
* <p>
* Essentially there are two components to the state,
* <ol>
* <li>the fragment, which is the entire URI after the '#'</li>
* <li>a set of component parts (virtualPage, parameters and path segments)</li>
* </ol>
* <p>
* To keep these consistent, you will need to use a {@link URIFragmentHandler}. So for example, if you have a new
* fragment to use, simply call {@link URIFragmentHandler#navigationState(String)} to create a new NavigationState
* instance.
* <p>
* If you want to modify an existing navigation state, make the modification to one or more of its component parts and
* call {@link URIFragmentHandler#updateFragment(NavigationState)} to make the fragment consistent again. For example,
* to redirect to another 'page', but retain the parameters:
* <p>
* <code>navigationState.setVirtualPage("another/newpage");<br>
* uriFragmentHandler.updateFragment(navigationState);</code>
* <p>
* This is functionally the same as
* <p>
* <code>navigationState.setVirtualPage("another/newpage");<br>
* navigationState.setFragment(uriFragmentHandler.fragment(navigationState));
* </code>
* <p>
* A NavigationState 'a' is equal to NavigationState 'b' if a.getFragment.equals(b.getFragment())
*
* @author David Sowerby
*/
public class NavigationState implements Serializable {
private static Logger log = getLogger(NavigationState.class);
private final Map<String, String> parameters = new LinkedHashMap<String, String>();
// fragment is out of date
private boolean fragmentChanged;
private boolean partsChanged;
private String fragment;
private List<String> pathSegments;
private String virtualPage;
private boolean updateInProgress;
@Inject
public NavigationState() {
super();
}
public void setUpdateInProgress(boolean updateInProgress) {
this.updateInProgress = updateInProgress;
}
public String getFragment() {
validStateCheck();
return fragment;
}
/**
* use {@link #fragment(String)}
*
* @param fragment
*/
@Deprecated
public void setFragment(@Nonnull String fragment) {
fragment(fragment);
}
public NavigationState fragment(@Nonnull String fragment) {
checkNotNull(fragment);
this.fragment = fragment;
fragmentChanged = true;
return this;
}
public String getVirtualPage() {
validStateCheck();
return virtualPage;
}
/**
* Use virtualPage(String) instead
*/
@Deprecated
public void setVirtualPage(@Nonnull String virtualPage) {
virtualPage(virtualPage);
}
public String getUriSegment() {
validStateCheck();
return pathSegments.get(pathSegments.size() - 1);
}
public boolean isDirty() {
return fragmentChanged || partsChanged;
}
public Map<String, String> getParameters() {
validStateCheck();
return ImmutableMap.copyOf(parameters);
}
public List<String> getParameterList() {
validStateCheck();
return parameters.entrySet()
.stream()
.map(entry -> entry.getKey() + '=' + entry.getValue())
.collect(Collectors.toList());
}
private void validStateCheck() {
if (isDirty() && !(updateInProgress)) {
throw new NavigationStateException("The navigation state is inconsistent, call update() before accessing");
}
}
public String getParameterValue(@Nonnull String key) {
validStateCheck();
checkNotNull(key);
return parameters.get(key);
}
@Nonnull
public List<String> getPathSegments() {
validStateCheck();
return pathSegments == null ? ImmutableList.of() : ImmutableList.copyOf(pathSegments);
}
/**
* use {#virtualPage(String} instead
*/
@Deprecated
public void setPathSegments(@Nonnull List<String> pathSegments) {
pathSegments(pathSegments);
}
public NavigationState pathSegments(@Nonnull List<String> pathSegments) {
checkNotNull(pathSegments);
this.pathSegments = pathSegments;
partsChanged = true;
if (!updateInProgress) {
virtualPage = null;
}
return this;
}
/**
* Use {@link #parameter(String, String)}
*/
@Deprecated
public void setParameterValue(@Nonnull String key, @Nonnull String value) {
parameter(key, value);
}
/**
* Use {@link #parameter(String, String)}
*/
@Deprecated
public void addParameter(@Nonnull String key, @Nonnull String value) {
parameter(key, value);
}
public NavigationState removeParameter(@Nonnull String key) {
checkNotNull(key);
String result = parameters.remove(key);
if (result != null) {
partsChanged = true;
}
return this;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
return prime * result + ((fragment == null) ? 0 : fragment.hashCode());
}
@Override
public boolean equals(Object obj) {
validStateCheck();
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
NavigationState other = (NavigationState) obj;
if (fragment == null) {
if (other.getFragment() != null) return false;
} else if (!fragment.equals(other.getFragment())) return false;
return true;
}
public boolean isFragmentChanged() {
return fragmentChanged;
}
public boolean isPartsChanged() {
return partsChanged;
}
public boolean hasParameter(@Nonnull String parameterName) {
validStateCheck();
checkNotNull(parameterName);
return parameters.containsKey(parameterName);
}
public NavigationState virtualPage(@Nonnull final String virtualPage) {
checkNotNull(virtualPage);
checkNotNull(virtualPage);
this.virtualPage = virtualPage;
partsChanged = true;
if (!updateInProgress) {
pathSegments = null;
}
return this;
}
public NavigationState parameter(@Nonnull String key, @Nonnull String value) {
checkNotNull(key);
checkNotNull(value);
parameters.put(key, value);
partsChanged = true;
return this;
}
/**
* This appears a bit circular, but the {@link URIFragmentHandler} sets the rules for how fragments are constructed. Updates according to whether
* {@link #fragmentChanged} or {@link #partsChanged} is true. If both are true, {@link #fragmentChanged} takes precedence
*
* @param handler the fragment handler used to make the update
* @return this for fluency
*/
public void update(URIFragmentHandler handler) {
updateInProgress = true;
if (fragmentChanged) {
handler.updateParts(this);
} else {
if (partsChanged) {
handler.updateFragment(this);
}
}
log.debug("State has not been changed, no update needed");
}
public void updated() {
fragmentChanged = false;
partsChanged = false;
updateInProgress = false;
}
}