/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.isis.viewer.wicket.ui.components.scalars.reference;
import java.util.List;
import com.google.common.collect.Lists;
import org.apache.wicket.AttributeModifier;
import org.apache.wicket.Component;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.validation.IValidatable;
import org.apache.wicket.validation.IValidator;
import org.apache.wicket.validation.ValidationError;
import org.wicketstuff.select2.ChoiceProvider;
import org.wicketstuff.select2.Settings;
import org.apache.isis.applib.annotation.PromptStyle;
import org.apache.isis.core.commons.config.IsisConfiguration;
import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager.ConcurrencyChecking;
import org.apache.isis.core.metamodel.facets.object.autocomplete.AutoCompleteFacet;
import org.apache.isis.core.metamodel.spec.ObjectSpecification;
import org.apache.isis.viewer.wicket.model.isis.WicketViewerSettings;
import org.apache.isis.viewer.wicket.model.mementos.ObjectAdapterMemento;
import org.apache.isis.viewer.wicket.model.models.EntityModel;
import org.apache.isis.viewer.wicket.model.models.ScalarModel;
import org.apache.isis.viewer.wicket.ui.ComponentFactory;
import org.apache.isis.viewer.wicket.ui.ComponentType;
import org.apache.isis.viewer.wicket.ui.components.scalars.PanelWithChoices;
import org.apache.isis.viewer.wicket.ui.components.scalars.ScalarPanelAbstract2;
import org.apache.isis.viewer.wicket.ui.components.scalars.ScalarPanelSelect2Abstract;
import org.apache.isis.viewer.wicket.ui.components.widgets.bootstrap.FormGroup;
import org.apache.isis.viewer.wicket.ui.components.widgets.entitysimplelink.EntityLinkSimplePanel;
import org.apache.isis.viewer.wicket.ui.components.widgets.select2.Select2;
import org.apache.isis.viewer.wicket.ui.components.widgets.select2.providers.ObjectAdapterMementoProviderForReferenceChoices;
import org.apache.isis.viewer.wicket.ui.components.widgets.select2.providers.ObjectAdapterMementoProviderForReferenceObjectAutoComplete;
import org.apache.isis.viewer.wicket.ui.components.widgets.select2.providers.ObjectAdapterMementoProviderForReferenceParamOrPropertyAutoComplete;
import org.apache.isis.viewer.wicket.ui.util.Components;
import org.apache.isis.viewer.wicket.ui.util.CssClassAppender;
/**
* Panel for rendering scalars which of are of reference type (as opposed to
* value types).
*/
public class ReferencePanel extends ScalarPanelSelect2Abstract implements PanelWithChoices {
private static final long serialVersionUID = 1L;
private static final String ID_AUTO_COMPLETE = "autoComplete";
private static final String ID_ENTITY_ICON_TITLE = "entityIconAndTitle";
/**
* Determines the behaviour of dependent choices for the dependent; either to autoselect the first available choice, or to select none.
*/
private static final String KEY_DISABLE_DEPENDENT_CHOICE_AUTO_SELECTION = "isis.viewer.wicket.disableDependentChoiceAutoSelection";
private EntityLinkSelect2Panel entityLink;
private EntityLinkSimplePanel entitySimpleLink;
public ReferencePanel(final String id, final ScalarModel scalarModel) {
super(id, scalarModel);
}
Select2 getSelect2() {
return select2;
}
// //////////////////////////////////////
// First called as a side-effect of {@link #beforeRender()}
@Override
protected Component createComponentForCompact() {
final ScalarModel scalarModel = getModel();
final String name = scalarModel.getName();
entitySimpleLink = (EntityLinkSimplePanel) getComponentFactoryRegistry().createComponent(ComponentType.ENTITY_LINK, getModel());
entitySimpleLink.setOutputMarkupId(true);
entitySimpleLink.setLabel(Model.of(name));
final WebMarkupContainer labelIfCompact = new WebMarkupContainer(ID_SCALAR_IF_COMPACT);
labelIfCompact.add(entitySimpleLink);
return labelIfCompact;
}
// First called as a side-effect of {@link #beforeRender()}
@Override
protected FormGroup createComponentForRegular() {
entityLink = new EntityLinkSelect2Panel(ComponentType.ENTITY_LINK.getWicketId(), this);
entityLink.setRequired(getModel().isRequired());
this.select2 = createSelect2AndSemantics();
entityLink.addOrReplace(select2.component());
syncWithInput();
setOutputMarkupId(true);
entityLink.setOutputMarkupId(true);
select2.component().setOutputMarkupId(true);
final String name = scalarModel.getName();
select2.setLabel(Model.of(name));
final FormGroup formGroup = createFormGroupAndName(this.entityLink, ID_SCALAR_IF_REGULAR, ID_SCALAR_NAME);
// add semantics
this.entityLink.setRequired(getModel().isRequired());
this.entityLink.add(new IValidator<ObjectAdapter>() {
private static final long serialVersionUID = 1L;
@Override
public void validate(final IValidatable<ObjectAdapter> validatable) {
final ObjectAdapter proposedAdapter = validatable.getValue();
final String reasonIfAny = getModel().validate(proposedAdapter);
if (reasonIfAny != null) {
final ValidationError error = new ValidationError();
error.setMessage(reasonIfAny);
validatable.error(error);
}
}
});
return formGroup;
}
@Override
protected Component getScalarValueComponent() {
return select2.component();
}
private Select2 createSelect2AndSemantics() {
final Select2 select2 = createSelect2(ID_AUTO_COMPLETE);
final Settings settings = select2.getSettings();
// one of these three case should be true
// (as per the isEditableWithEitherAutoCompleteOrChoices() guard above)
if(getModel().hasChoices()) {
settings.setPlaceholder(getModel().getName());
} else if(getModel().hasAutoComplete()) {
final int minLength = getModel().getAutoCompleteMinLength();
settings.setMinimumInputLength(minLength);
settings.setPlaceholder(getModel().getName());
} else if(hasObjectAutoComplete()) {
final ObjectSpecification typeOfSpecification = getModel().getTypeOfSpecification();
final AutoCompleteFacet autoCompleteFacet = typeOfSpecification.getFacet(AutoCompleteFacet.class);
final int minLength = autoCompleteFacet.getMinLength();
settings.setMinimumInputLength(minLength);
}
return select2;
}
// //////////////////////////////////////
@Override
protected InlinePromptConfig getInlinePromptConfig() {
return InlinePromptConfig.supportedAndHide(
scalarModel.getMode() == EntityModel.Mode.EDIT ||
scalarModel.getKind() == ScalarModel.Kind.PARAMETER
? select2.component()
: null);
}
@Override
protected IModel<String> obtainInlinePromptModel() {
final IModel<ObjectAdapterMemento> model = select2.getModel();
return new IModel<String>() {
@Override
public String getObject() {
final ObjectAdapterMemento oam = model.getObject();
if(oam == null) {
return null;
}
ObjectAdapter objectAdapter = oam
.getObjectAdapter(ConcurrencyChecking.NO_CHECK, getPersistenceSession(),
getSpecificationLoader());
return objectAdapter != null ? objectAdapter.titleString(null) : null;
}
@Override
public void setObject(final String s) {
// gnore
}
@Override
public void detach() {
// ignore
}
};
}
// //////////////////////////////////////
// onBeforeRender*
// //////////////////////////////////////
@Override
protected void onInitializeWhenEnabled() {
super.onInitializeWhenEnabled();
entityLink.setEnabled(true);
syncWithInput();
}
@Override
protected void onInitializeWhenViewMode() {
super.onInitializeWhenViewMode();
entityLink.setEnabled(false);
syncWithInput();
}
@Override
protected void onInitializeWhenDisabled(final String disableReason) {
super.onInitializeWhenDisabled(disableReason);
syncWithInput();
final EntityModel entityLinkModel = (EntityModel) entityLink.getModel();
entityLinkModel.toViewMode();
entityLink.setEnabled(false);
entityLink.add(new AttributeModifier("title", Model.of(disableReason)));
}
// //////////////////////////////////////
// syncWithInput
// //////////////////////////////////////
// called from onBeforeRender*
// (was previous called by EntityLinkSelect2Panel in onBeforeRender, this responsibility now moved)
private void syncWithInput() {
final ObjectAdapter adapter = getModel().getPendingElseCurrentAdapter();
// syncLinkWithInput
final MarkupContainer componentForRegular = (MarkupContainer) getComponentForRegular();
if (adapter != null) {
if(componentForRegular != null) {
final EntityModel entityModelForLink = new EntityModel(adapter);
entityModelForLink.setContextAdapterIfAny(getModel().getContextAdapterIfAny());
entityModelForLink.setRenderingHint(getModel().getRenderingHint());
final ComponentFactory componentFactory =
getComponentFactoryRegistry().findComponentFactory(ComponentType.ENTITY_ICON_AND_TITLE, entityModelForLink);
final Component component = componentFactory.createComponent(ComponentType.ENTITY_ICON_AND_TITLE.getWicketId(), entityModelForLink);
if(scalarModel.getPromptStyle() == PromptStyle.INLINE && scalarModel.canEnterEditMode()) {
// bit of a hack... allows us to suppress the title using CSS
component.add(new CssClassAppender("inlinePrompt"));
}
componentForRegular.addOrReplace(component);
Components.permanentlyHide(componentForRegular, "entityTitleIfNull");
}
} else {
if(componentForRegular != null) {
componentForRegular.addOrReplace(new Label("entityTitleIfNull", "(none)"));
//Components.permanentlyHide(componentForRegular, "entityTitleIfNull");
Components.permanentlyHide(componentForRegular, ID_ENTITY_ICON_TITLE);
}
}
// syncLinkWithInputIfAutoCompleteOrChoices
if(isEditableWithEitherAutoCompleteOrChoices()) {
if(select2 == null) {
throw new IllegalStateException("select2 should be created already");
} else {
//
// the select2Choice already exists, so the widget has been rendered before. If it is
// being re-rendered now, it may be because some other property/parameter was invalid.
// when the form was submitted, the selected object (its oid as a string) would have
// been saved as rawInput. If the property/parameter had been valid, then this rawInput
// would be correctly converted and processed by the select2Choice's choiceProvider. However,
// an invalid property/parameter means that the webpage is re-rendered in another request,
// and the rawInput can no longer be interpreted. The net result is that the field appears
// with no input.
//
// The fix is therefore (I think) simply to clear any rawInput, so that the select2Choice
// renders its state from its model.
//
// see: FormComponent#getInputAsArray()
// see: Select2Choice#renderInitializationScript()
//
select2.clearInput();
}
if(getComponentForRegular() != null) {
Components.permanentlyHide((MarkupContainer)getComponentForRegular(), ID_ENTITY_ICON_TITLE);
Components.permanentlyHide(componentForRegular, "entityTitleIfNull");
}
// syncUsability
if(select2 != null) {
final boolean mutability = entityLink.isEnableAllowed() && !getModel().isViewMode();
select2.setEnabled(mutability);
}
Components.permanentlyHide(entityLink, "entityLinkIfNull");
} else {
// this is horrid; adds a label to the id
// should instead be a 'temporary hide'
Components.permanentlyHide(entityLink, ID_AUTO_COMPLETE);
// setSelect2(null); // this forces recreation next time around
}
}
// //////////////////////////////////////
// setProviderAndCurrAndPending
// //////////////////////////////////////
@Override
protected ChoiceProvider<ObjectAdapterMemento> buildChoiceProvider(final ObjectAdapter[] argsIfAvailable) {
if (getModel().hasChoices()) {
List<ObjectAdapterMemento> choiceMementos = obtainChoiceMementos(argsIfAvailable);
return new ObjectAdapterMementoProviderForReferenceChoices(getModel(), wicketViewerSettings, choiceMementos);
}
if(getModel().hasAutoComplete()) {
return new ObjectAdapterMementoProviderForReferenceParamOrPropertyAutoComplete(getModel(), wicketViewerSettings);
}
return new ObjectAdapterMementoProviderForReferenceObjectAutoComplete(getModel(), wicketViewerSettings);
}
// called by setProviderAndCurrAndPending
private List<ObjectAdapterMemento> obtainChoiceMementos(final ObjectAdapter[] argsIfAvailable) {
final List<ObjectAdapter> choices = Lists.newArrayList();
if(getModel().hasChoices()) {
choices.addAll(getModel().getChoices(argsIfAvailable, getAuthenticationSession(), getDeploymentCategory()));
}
// take a copy (otherwise is only lazily evaluated)
return Lists.newArrayList(Lists.transform(choices, ObjectAdapterMemento.Functions.fromAdapter()));
}
// called by setProviderAndCurrAndPending
@Override
protected void resetIfCurrentNotInChoices(final Select2 select2, final List<ObjectAdapterMemento> choiceMementos) {
final ObjectAdapterMemento curr = select2.getModelObject();
if(!getModel().isCollection()) {
if(curr == null) {
select2.getModel().setObject(null);
getModel().setObject(null);
return;
}
if(!curr.containedIn(choiceMementos, getPersistenceSession(), getSpecificationLoader())) {
if(!choiceMementos.isEmpty() && autoSelect()) {
final ObjectAdapterMemento newAdapterMemento = choiceMementos.get(0);
select2.getModel().setObject(newAdapterMemento);
getModel().setObject(newAdapterMemento.getObjectAdapter(ConcurrencyChecking.NO_CHECK,
getPersistenceSession(), getSpecificationLoader()));
} else {
select2.getModel().setObject(null);
getModel().setObject(null);
}
}
} else {
// TODO
}
}
private boolean autoSelect() {
final boolean disableAutoSelect = getConfiguration().getBoolean(KEY_DISABLE_DEPENDENT_CHOICE_AUTO_SELECTION, false);
final boolean autoSelect = !disableAutoSelect;
return autoSelect;
}
// //////////////////////////////////////
// getInput, convertInput
// //////////////////////////////////////
// called by EntityLinkSelect2Panel
String getInput() {
final ObjectAdapter pendingElseCurrentAdapter = getModel().getPendingElseCurrentAdapter();
return pendingElseCurrentAdapter != null? pendingElseCurrentAdapter.titleString(null): "(no object)";
}
// //////////////////////////////////////
// called by EntityLinkSelect2Panel
void convertInput() {
if(isEditableWithEitherAutoCompleteOrChoices()) {
// flush changes to pending
ObjectAdapterMemento convertedInput =
select2.getConvertedInput();
getModel().setPending(convertedInput);
if(select2 != null) {
select2.getModel().setObject(convertedInput);
}
final ObjectAdapter adapter = convertedInput!=null?convertedInput.getObjectAdapter(ConcurrencyChecking.NO_CHECK,
getPersistenceSession(), getSpecificationLoader()):null;
getModel().setObject(adapter);
}
final ObjectAdapter pendingAdapter = getModel().getPendingAdapter();
entityLink.setConvertedInput(pendingAdapter);
}
// //////////////////////////////////////
@Override
public void onUpdate(
final AjaxRequestTarget target, final ScalarPanelAbstract2 scalarPanel) {
super.onUpdate(target, scalarPanel);
target.appendJavaScript(
String.format("Wicket.Event.publish(Isis.Topic.CLOSE_SELECT2, '%s')", getMarkupId()));
}
// //////////////////////////////////////
// helpers querying model state
// //////////////////////////////////////
// called from convertInput, syncWithInput
private boolean isEditableWithEitherAutoCompleteOrChoices() {
if(getModel().getRenderingHint().isInTable()) {
return false;
}
// doesn't apply if not editable, either
if(getModel().isViewMode()) {
return false;
}
return getModel().hasChoices() || getModel().hasAutoComplete() || hasObjectAutoComplete();
}
// called by isEditableWithEitherAutoCompleteOrChoices
private boolean hasObjectAutoComplete() {
final ObjectSpecification typeOfSpecification = getModel().getTypeOfSpecification();
final AutoCompleteFacet autoCompleteFacet =
(typeOfSpecification != null)? typeOfSpecification.getFacet(AutoCompleteFacet.class):null;
return autoCompleteFacet != null;
}
@Override
protected String getScalarPanelType() {
return "referencePanel";
}
// //////////////////////////////////////
@com.google.inject.Inject
WicketViewerSettings wicketViewerSettings;
IsisConfiguration getConfiguration() {
return getIsisSessionFactory().getConfiguration();
}
}