diff --git a/wicket-core/src/main/java/org/apache/wicket/Behaviors.java b/wicket-core/src/main/java/org/apache/wicket/Behaviors.java deleted file mode 100644 index aceb318a868..00000000000 --- a/wicket-core/src/main/java/org/apache/wicket/Behaviors.java +++ /dev/null @@ -1,325 +0,0 @@ -/* - * 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.wicket; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.apache.wicket.behavior.Behavior; -import org.apache.wicket.behavior.InvalidBehaviorIdException; -import org.apache.wicket.model.IDetachable; -import org.apache.wicket.util.lang.Args; - -/** - * Manages behaviors in a {@link Component} instance - * - * @author igor - */ -final class Behaviors implements IDetachable -{ - private static final long serialVersionUID = 1L; - private final Component component; - - public Behaviors(Component component) - { - this.component = component; - } - - public void add(Behavior... behaviors) - { - Args.notNull(behaviors, "behaviors"); - - for (Behavior behavior : behaviors) - { - Args.notNull(behavior, "behavior"); - - internalAdd(behavior); - - if (!behavior.isTemporary(component)) - { - component.addStateChange(); - } - - // Give handler the opportunity to bind this component - behavior.bind(component); - } - } - - private void internalAdd(final Behavior behavior) - { - component.data_add(behavior); - if (behavior.getStatelessHint(component) == false) - { - getBehaviorId(behavior); - } - } - - @SuppressWarnings("unchecked") - public List getBehaviors(Class type) - { - final int len = component.data_length(); - final int start = component.data_start(); - if (len < start) - { - return Collections.emptyList(); - } - - List subset = new ArrayList<>(len); - for (int i = component.data_start(); i < len; i++) - { - Object obj = component.data_get(i); - if (obj != null && obj instanceof Behavior) - { - if (type == null || type.isAssignableFrom(obj.getClass())) - { - subset.add((M)obj); - } - } - } - if (subset.isEmpty()) { - return Collections.emptyList(); - } - return Collections.unmodifiableList(subset); - } - - - public void remove(Behavior behavior) - { - Args.notNull(behavior, "behavior"); - - if (internalRemove(behavior)) - { - if (!behavior.isTemporary(component)) - { - component.addStateChange(); - } - behavior.detach(component); - } - else - { - throw new IllegalStateException( - "Tried to remove a behavior that was not added to the component. Behavior: " + - behavior.toString()); - } - } - - /** - * THIS IS WICKET INTERNAL ONLY. DO NOT USE IT. - * - * Traverses all behaviors and calls detachModel() on them. This is needed to cleanup behavior - * after render. This method is necessary for {@link org.apache.wicket.ajax.AjaxRequestTarget} to be able to cleanup - * component's behaviors after header contribution has been done (which is separated from - * component render). - */ - @Override - public final void detach() - { - int len = component.data_length(); - for (int i = component.data_start(); i < len; i++) - { - Object obj = component.data_get(i); - if (obj != null && obj instanceof Behavior) - { - final Behavior behavior = (Behavior)obj; - - behavior.detach(component); - - if (behavior.isTemporary(component)) - { - internalRemove(behavior); - i--; - len--; - } - } - } - } - - private boolean internalRemove(final Behavior behavior) - { - final int len = component.data_length(); - for (int i = component.data_start(); i < len; i++) - { - Object o = component.data_get(i); - if (o != null && o.equals(behavior)) - { - component.data_remove(i); - behavior.unbind(component); - - // remove behavior from behavior-ids - ArrayList ids = getBehaviorsIdList(false); - if (ids != null) - { - int idx = ids.indexOf(behavior); - if (idx == ids.size() - 1) - { - ids.remove(idx); - } - else if (idx >= 0) - { - ids.set(idx, null); - } - ids.trimToSize(); - - if (ids.isEmpty()) - { - removeBehaviorsIdList(); - } - - } - return true; - } - } - return false; - } - - private void removeBehaviorsIdList() - { - for (int i = component.data_start(); i < component.data_length(); i++) - { - Object obj = component.data_get(i); - if (obj != null && obj instanceof BehaviorIdList) - { - component.data_remove(i); - return; - } - } - } - - private BehaviorIdList getBehaviorsIdList(boolean createIfNotFound) - { - int len = component.data_length(); - for (int i = component.data_start(); i < len; i++) - { - Object obj = component.data_get(i); - if (obj != null && obj instanceof BehaviorIdList) - { - return (BehaviorIdList)obj; - } - } - if (createIfNotFound) - { - BehaviorIdList list = new BehaviorIdList(); - component.data_add(list); - return list; - } - return null; - } - - /** - * Called when the component is going to be removed. Notifies all - * behaviors assigned to this component. - * - * @param component - * the component that will be removed from its parent - */ - public void onRemove(Component component) - { - final int len = component.data_length(); - for (int i = component.data_start(); i < len; i++) - { - Object obj = component.data_get(i); - if (obj != null && obj instanceof Behavior) - { - final Behavior behavior = (Behavior)obj; - - behavior.onRemove(component); - } - } - } - - private static class BehaviorIdList extends ArrayList - { - private static final long serialVersionUID = 1L; - - public BehaviorIdList() - { - super(1); - } - } - - public final int getBehaviorId(Behavior behavior) - { - Args.notNull(behavior, "behavior"); - - boolean found = false; - for (int i = component.data_start(); i < component.data_length(); i++) - { - if (behavior == component.data_get(i)) - { - found = true; - break; - } - } - if (!found) - { - throw new IllegalStateException( - "Behavior must be added to component before its id can be generated. Behavior: " + - behavior + ", Component: " + this); - } - - ArrayList ids = getBehaviorsIdList(true); - - int id = ids.indexOf(behavior); - - if (id < 0) - { - // try to find an unused slot - for (int i = 0; i < ids.size(); i++) - { - if (ids.get(i) == null) - { - ids.set(i, behavior); - id = i; - break; - } - } - } - - if (id < 0) - { - // no unused slots, add to the end - id = ids.size(); - ids.add(behavior); - ids.trimToSize(); - } - - return id; - } - - public final Behavior getBehaviorById(int id) - { - Behavior behavior = null; - - ArrayList ids = getBehaviorsIdList(false); - if (ids != null) - { - if (id >= 0 && id < ids.size()) - { - behavior = ids.get(id); - } - } - - if (behavior != null) - { - return behavior; - } - throw new InvalidBehaviorIdException(component, id); - } - - -} diff --git a/wicket-core/src/main/java/org/apache/wicket/Component.java b/wicket-core/src/main/java/org/apache/wicket/Component.java index ef9dc414826..6a81617ea8f 100644 --- a/wicket-core/src/main/java/org/apache/wicket/Component.java +++ b/wicket-core/src/main/java/org/apache/wicket/Component.java @@ -381,11 +381,17 @@ public boolean compare(Component component, Object b) /** * Flag that determines whether the model is set. This is necessary because of the way we * represent component state ({@link #data}). We can't distinguish between model and behavior - * using instanceof, because one object can implement both interfaces. Thus we need this flag - - * when the flag is set, first object in {@link #data} is always model. + * using instanceof, because one object can implement both interfaces. */ private static final int FLAG_MODEL_SET = 0x100000; + /** + * Flag that is set when {@link #getBehaviorId(Behavior)} is called on this component. Once this + * flag is set, the indexes of all behaviors must remain fixed to keep the contract of + * {@link #getBehaviorId(Behavior)}. + */ + private static final int FLAG_BEHAVIOR_IDS_FIXED = 0x200000; + /** * Flag that restricts visibility of a component when set to true. This is usually used when a * component wants to restrict visibility of another component. Calling @@ -456,8 +462,7 @@ public boolean compare(Component component, Object b) /** * Instead of remembering the whole markupId, we just remember the number for this component so - * we can "reconstruct" the markupId on demand. While this could be part of {@link #data}, - * profiling showed that having it as separate property consumes less memory. + * we can "reconstruct" the markupId on demand. */ int generatedMarkupId = -1; @@ -480,170 +485,11 @@ public boolean compare(Component component, Object b) *
  • MetaDataEntry (optionally {@link MetaDataEntry}[] if more metadata entries are present) * *
  • {@link Behavior}(s) added to component. The behaviors are not stored in separate array, * they are part of the {@link #data} array (this is in order to save the space of the pointer - * to an empty array as most components have no behaviours). - FIXME - explain why - is this - * correct? + * to an empty array as most components have no behaviours). + *
  • A {@link ComponentState} if a combination of the attributes is set. * - * If there is only one attribute set (i.e. model or MetaDataEntry([]) or one behavior), the - * #data object points directly to value of that attribute. Otherwise the data is of type - * Object[] where the attributes are ordered as specified above. - *

    */ - Object data = null; - - final int data_start() - { - return getFlag(FLAG_MODEL_SET) ? 1 : 0; - } - - final int data_length() - { - if (data == null) - { - return 0; - } - else if (data instanceof Object[] && !(data instanceof MetaDataEntry[])) - { - return ((Object[])data).length; - } - else - { - return 1; - } - } - - final Object data_get(int index) - { - if (data == null) - { - return null; - } - else if (data instanceof Object[] && !(data instanceof MetaDataEntry[])) - { - Object[] array = (Object[])data; - return index < array.length ? array[index] : null; - } - else if (index == 0) - { - return data; - } - else - { - return null; - } - } - - final void data_set(int index, Object object) - { - if (index > data_length() - 1) - { - throw new IndexOutOfBoundsException("can not set data at " + index + - " when data_length() is " + data_length()); - } - else if (index == 0 && !(data instanceof Object[] && !(data instanceof MetaDataEntry[]))) - { - data = object; - } - else - { - Object[] array = (Object[])data; - array[index] = object; - } - } - - final void data_add(Object object) - { - data_insert(-1, object); - } - - final void data_insert(int position, Object object) - { - int currentLength = data_length(); - if (position == -1) - { - position = currentLength; - } - if (position > currentLength) - { - throw new IndexOutOfBoundsException("can not insert data at " + position + - " when data_length() is " + currentLength); - } - if (currentLength == 0) - { - data = object; - } - else if (currentLength == 1) - { - Object[] array = new Object[2]; - if (position == 0) - { - array[0] = object; - array[1] = data; - } - else - { - array[0] = data; - array[1] = object; - } - data = array; - } - else - { - Object[] array = new Object[currentLength + 1]; - Object[] current = (Object[])data; - int after = currentLength - position; - if (position > 0) - { - System.arraycopy(current, 0, array, 0, position); - } - array[position] = object; - if (after > 0) - { - System.arraycopy(current, position, array, position + 1, after); - } - data = array; - } - } - - final void data_remove(int position) - { - int currentLength = data_length(); - - if (position > currentLength - 1) - { - throw new IndexOutOfBoundsException(); - } - else if (currentLength == 1) - { - data = null; - } - else if (currentLength == 2) - { - Object[] current = (Object[])data; - if (position == 0) - { - data = current[1]; - } - else - { - data = current[0]; - } - } - else - { - Object[] current = (Object[])data; - data = new Object[currentLength - 1]; - - if (position > 0) - { - System.arraycopy(current, 0, data, 0, position); - } - if (position != currentLength - 1) - { - final int left = currentLength - position - 1; - System.arraycopy(current, position + 1, data, position, left); - } - } - } + private Object data = null; /** * Constructor. All components have names. A component's id cannot be null. This is the minimal @@ -1090,7 +936,7 @@ final void internalOnRemove() getClass().getName() + " has not called super.onRemove() in the override of onRemove() method"); } - new Behaviors(this).onRemove(this); + ComponentState.onRemoveBehaviors(this, data, getFlag(FLAG_MODEL_SET)); removeChildren(); } @@ -1119,7 +965,8 @@ public final void detach() detachModels(); // detach any behaviors - new Behaviors(this).detach(); + data = ComponentState.detachBehaviors(this, data, getFlag(FLAG_MODEL_SET), + getFlag(FLAG_BEHAVIOR_IDS_FIXED)); } catch (Exception x) { @@ -1512,39 +1359,20 @@ public String getMarkupId() * @see MetaDataKey */ @Override + @SuppressWarnings("unchecked") public final M getMetaData(final MetaDataKey key) { - return key.get(getMetaData()); - } - - /** - * Gets the meta data entries for this component as an array of {@link MetaDataEntry} objects. - * - * @return the meta data entries for this component - */ - private MetaDataEntry[] getMetaData() - { - MetaDataEntry[] metaData = null; - - // index where we should expect the entry - int index = getFlag(FLAG_MODEL_SET) ? 1 : 0; - - int length = data_length(); - - if (index < length) + Object metaData = ComponentState.getMetaData(data, getFlag(FLAG_MODEL_SET)); + if (metaData == null) { - Object object = data_get(index); - if (object instanceof MetaDataEntry[]) - { - metaData = (MetaDataEntry[])object; - } - else if (object instanceof MetaDataEntry) - { - metaData = new MetaDataEntry[] { (MetaDataEntry)object }; - } + return null; } - - return metaData; + else if (metaData instanceof MetaDataEntry) + { + MetaDataEntry< ? > entry = (MetaDataEntry< ? >) metaData; + return entry.key.equals(key) ? (M) entry.object : null; + } + return key.get((MetaDataEntry< ? >[]) metaData); } /** @@ -2872,29 +2700,7 @@ public Component setMarkupId(String markupId) @Override public final Component setMetaData(final MetaDataKey key, final M object) { - MetaDataEntry[] old = getMetaData(); - - Object metaData = null; - MetaDataEntry[] metaDataArray = key.set(getMetaData(), object); - if (metaDataArray != null && metaDataArray.length > 0) - { - metaData = (metaDataArray.length > 1) ? metaDataArray : metaDataArray[0]; - } - - int index = getFlag(FLAG_MODEL_SET) ? 1 : 0; - - if (old == null && metaData != null) - { - data_insert(index, metaData); - } - else if (old != null && metaData != null) - { - data_set(index, metaData); - } - else if (old != null && metaData == null) - { - data_remove(index); - } + data = ComponentState.setMetaData(data, getFlag(FLAG_MODEL_SET), key, object); return this; } @@ -2944,11 +2750,7 @@ public Component setDefaultModel(final IModel model) */ IModel getModelImpl() { - if (getFlag(FLAG_MODEL_SET)) - { - return (IModel)data_get(0); - } - return null; + return ComponentState.getModel(data, getFlag(FLAG_MODEL_SET)); } /** @@ -2957,26 +2759,8 @@ IModel getModelImpl() */ void setModelImpl(IModel model) { - if (getFlag(FLAG_MODEL_SET)) - { - if (model != null) - { - data_set(0, model); - } - else - { - data_remove(0); - setFlag(FLAG_MODEL_SET, false); - } - } - else - { - if (model != null) - { - data_insert(0, model); - setFlag(FLAG_MODEL_SET, true); - } - } + data = ComponentState.setModel(model, data, getFlag(FLAG_MODEL_SET)); + setFlag(FLAG_MODEL_SET, model != null); } /** @@ -3620,7 +3404,7 @@ protected final Page findPage() */ public List getBehaviors(Class type) { - return new Behaviors(this).getBehaviors(type); + return ComponentState.getBehaviors(type, data, getFlag(FLAG_MODEL_SET)); } /** @@ -4429,11 +4213,7 @@ public final void send(IEventSink sink, Broadcast type, T payload) */ public Component remove(final Behavior... behaviors) { - Behaviors helper = new Behaviors(this); - for (Behavior behavior : behaviors) - { - helper.remove(behavior); - } + data = ComponentState.removeBehaviors(this, data, getFlag(FLAG_MODEL_SET), behaviors); return this; } @@ -4441,7 +4221,10 @@ public Component remove(final Behavior... behaviors) @Override public final Behavior getBehaviorById(int id) { - return new Behaviors(this).getBehaviorById(id); + data = ComponentState.compactBehaviors(this, data, getFlag(FLAG_MODEL_SET), + getFlag(FLAG_BEHAVIOR_IDS_FIXED)); + setFlag(FLAG_BEHAVIOR_IDS_FIXED, true); + return ComponentState.getBehaviorById(this, id, data, getFlag(FLAG_MODEL_SET)); } /** {@inheritDoc} */ @@ -4453,7 +4236,10 @@ public final int getBehaviorId(Behavior behavior) throw new IllegalArgumentException( "Cannot get a stable id for temporary behavior " + behavior); } - return new Behaviors(this).getBehaviorId(behavior); + data = ComponentState.compactBehaviors(this, data, getFlag(FLAG_MODEL_SET), + getFlag(FLAG_BEHAVIOR_IDS_FIXED)); + setFlag(FLAG_BEHAVIOR_IDS_FIXED, true); + return ComponentState.getBehaviorId(this, behavior, data, getFlag(FLAG_MODEL_SET)); } /** @@ -4465,7 +4251,11 @@ public final int getBehaviorId(Behavior behavior) */ public Component add(final Behavior... behaviors) { - new Behaviors(this).add(behaviors); + data = ComponentState.addBehaviors(this, data, getFlag(FLAG_MODEL_SET), behaviors); + for (Behavior curBehavior : behaviors) + { + ComponentState.bindBehavior(this, curBehavior); + } return this; } diff --git a/wicket-core/src/main/java/org/apache/wicket/ComponentState.java b/wicket-core/src/main/java/org/apache/wicket/ComponentState.java new file mode 100644 index 00000000000..363aa7c5616 --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/ComponentState.java @@ -0,0 +1,1081 @@ +/* + * 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.wicket; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.wicket.behavior.Behavior; +import org.apache.wicket.behavior.InvalidBehaviorIdException; +import org.apache.wicket.model.IModel; + +/** + * This class keeps track of the flexible state of a component: model, behaviors and meta data. + * These types of state vary per component. Not every component contains these elements or their + * numbers differ. The state is stored in the {@code data} field in {@link Component}. To keep the + * size of this state as small as possible, the following cases are identified: + *

      + *
    • No state at all: {@code data} is {@code null} + *
    • Only a model: {@code data} contains the model + *
    • Only one or more behaviors: {@code data} contains the behavior, or an array of behaviors + *
    • Only one or more meta data entries: {@code data} contains the entry, or an array of entries + *
    • A model and one or more behaviors: {@code data} contains an instance of + * {@link ModelBehaviorsComponentState} + *
    • A model and one or more meta data entries: {@code data} contains an instance of + * {@link ModelMetaDataComponentState} + *
    • One or more behaviors and one or more meta data entries: {@code data} contains an instance of + * {@link BehaviorsMetaDataComponentState} + *
    • A model, one or more behaviors and one or more meta data entries: {@code data} contains an + * instance of {@link ModelBehaviorsMetaDataComponentState} + *
    + * + * @author papegaaij + */ +abstract class ComponentState implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** + * @return The model stored by this state, or null. + */ + abstract IModel< ? > getModel(); + + /** + * @param model + * the new model or null + * @return the new state for the component using the rules defined above + */ + abstract Object setModel(IModel< ? > model); + + /** + * @return The behaviors stored by this state: null, a single behavior or an array of behaviors. + */ + abstract Object getBehaviors(); + + /** + * @param behaviors + * the new behaviors (null, one behavior or an array of behaviors) + * @return the new state for the component using the rules defined above + */ + abstract Object setBehaviors(Object behaviors); + + /** + * @return The meta data entries stored by this state: null, a single entry or an array of + * entries. + */ + abstract Object getMetaData(); + + /** + * @param metaData + * the new meta data entries (null, one entry or an array of entries) + * @return the new state for the component using the rules defined above + */ + abstract Object setMetaData(Object metaData); + + /** + * Combines a model and one or more behaviors. + */ + static class ModelBehaviorsComponentState extends ComponentState + { + private static final long serialVersionUID = 1L; + + private IModel< ? > model; + + private Object behaviors; + + private ModelBehaviorsComponentState(IModel< ? > model, Object behaviors) + { + this.model = model; + this.behaviors = behaviors; + } + + @Override + IModel< ? > getModel() + { + return model; + } + + @Override + Object setModel(IModel< ? > model) + { + if (model == null) + { + return behaviors; + } + this.model = model; + return this; + } + + @Override + Object getBehaviors() + { + return behaviors; + } + + @Override + Object setBehaviors(Object behaviors) + { + if (behaviors == null) + { + return model; + } + this.behaviors = behaviors; + return this; + } + + @Override + Object getMetaData() + { + return null; + } + + @Override + Object setMetaData(Object metaData) + { + if (metaData == null) + { + return this; + } + return new ModelBehaviorsMetaDataComponentState(model, behaviors, metaData); + } + } + + /** + * Combines a model and one or more meta data entries. + */ + static class ModelMetaDataComponentState extends ComponentState + { + private static final long serialVersionUID = 1L; + + private IModel< ? > model; + + private Object metaData; + + private ModelMetaDataComponentState(IModel< ? > model, Object metaData) + { + this.model = model; + this.metaData = metaData; + } + + @Override + IModel< ? > getModel() + { + return model; + } + + @Override + Object setModel(IModel< ? > model) + { + if (model == null) + { + return metaData; + } + this.model = model; + return this; + } + + @Override + Object getBehaviors() + { + return null; + } + + @Override + Object setBehaviors(Object behaviors) + { + if (behaviors == null) + { + return this; + } + return new ModelBehaviorsMetaDataComponentState(model, behaviors, metaData); + } + + @Override + Object getMetaData() + { + return metaData; + } + + @Override + Object setMetaData(Object metaData) + { + if (metaData == null) + { + return model; + } + this.metaData = metaData; + return this; + } + } + + /** + * Combines one or more behaviors and one ore more meta data entries. + */ + static class BehaviorsMetaDataComponentState extends ComponentState + { + private static final long serialVersionUID = 1L; + + private Object behaviors; + + private Object metaData; + + private BehaviorsMetaDataComponentState(Object behaviors, Object metaData) + { + this.behaviors = behaviors; + this.metaData = metaData; + } + + @Override + IModel< ? > getModel() + { + return null; + } + + @Override + Object setModel(IModel< ? > model) + { + if (model == null) + { + return this; + } + return new ModelBehaviorsMetaDataComponentState(model, behaviors, metaData); + } + + @Override + Object getBehaviors() + { + return behaviors; + } + + @Override + Object setBehaviors(Object behaviors) + { + if (behaviors == null) + { + return metaData; + } + this.behaviors = behaviors; + return this; + } + + @Override + Object getMetaData() + { + return metaData; + } + + @Override + Object setMetaData(Object metaData) + { + if (metaData == null) + { + return behaviors; + } + this.metaData = metaData; + return this; + } + } + + /** + * Combines a model, one or more behaviors and one or more meta data entries. + */ + static class ModelBehaviorsMetaDataComponentState extends ComponentState + { + private static final long serialVersionUID = 1L; + + private IModel< ? > model; + + private Object behaviors; + + private Object metaData; + + private ModelBehaviorsMetaDataComponentState(IModel< ? > model, Object behaviors, + Object metaData) + { + this.model = model; + this.behaviors = behaviors; + this.metaData = metaData; + } + + @Override + IModel< ? > getModel() + { + return model; + } + + @Override + Object setModel(IModel< ? > model) + { + if (model == null) + { + return new BehaviorsMetaDataComponentState(behaviors, metaData); + } + this.model = model; + return this; + } + + @Override + Object getBehaviors() + { + return behaviors; + } + + @Override + Object setBehaviors(Object behaviors) + { + if (behaviors == null) + { + return new ModelMetaDataComponentState(model, metaData); + } + this.behaviors = behaviors; + return this; + } + + @Override + Object getMetaData() + { + return metaData; + } + + @Override + Object setMetaData(Object metaData) + { + if (metaData == null) + { + return new ModelBehaviorsComponentState(model, behaviors); + } + this.metaData = metaData; + return this; + } + } + + /** + * @param state + * the component state + * @param modelSet + * a boolean indicating if the model is set + * @return the model from the given state or null + */ + static IModel< ? > getModel(Object state, boolean modelSet) + { + if (state instanceof ComponentState) + { + return ((ComponentState) state).getModel(); + } + return modelSet ? (IModel< ? >) state : null; + } + + /** + * @param state + * the component state + * @param modelSet + * a boolean indicating if the model is set + * @return the behaviors from the given state: null, one behavior or an array of behaviors + */ + static Object getBehaviors(Object state, boolean modelSet) + { + if (state instanceof ComponentState) + { + return ((ComponentState) state).getBehaviors(); + } + return modelSet || !(state instanceof Behavior || state instanceof Behavior[]) ? null + : state; + } + + /** + * @param state + * the component state + * @param modelSet + * a boolean indicating if the model is set + * @return the meta data entries from the given state: null, one entry or an array of entries + */ + static Object getMetaData(Object state, boolean modelSet) + { + if (state instanceof ComponentState) + { + return ((ComponentState) state).getMetaData(); + } + return modelSet || !(state instanceof MetaDataEntry || state instanceof MetaDataEntry[]) + ? null : state; + } + + /** + * Construct a new component state with the given model value + * + * @param model + * the new model to set or null to clear + * @param state + * the current component state + * @param modelSet + * a boolean indicating if the model is set + * @return the new component state + */ + static Object setModel(IModel< ? > model, Object state, boolean modelSet) + { + if (state instanceof ComponentState) + { + ComponentState compState = (ComponentState) state; + return compState.setModel(model); + } + else if (modelSet || state == null) + { + return model; + } + // state does not have a model, clear is a no-op + else if (model == null) + { + return state; + } + else if (state instanceof MetaDataEntry || state instanceof MetaDataEntry[]) + { + return new ModelMetaDataComponentState(model, state); + } + else + { + return new ModelBehaviorsComponentState(model, state); + } + } + + /** + * Construct a new component state with the given behaviors added + * + * @param component + * the component to add the behaviors to + * @param state + * the current component state + * @param modelSet + * a boolean indicating if the model is set + * @param behaviorsToAdd + * the behaviors to add + * @return the new component state + */ + static Object addBehaviors(Component component, Object state, boolean modelSet, + Behavior... behaviorsToAdd) + { + if (behaviorsToAdd.length == 0) + { + return state; + } + else if (state instanceof ComponentState) + { + ComponentState compState = (ComponentState) state; + return compState + .setBehaviors(addBehaviors(component, compState.getBehaviors(), behaviorsToAdd)); + } + else if (modelSet) + { + return new ModelBehaviorsComponentState((IModel< ? >) state, + addBehaviors(component, null, behaviorsToAdd)); + } + else if (state instanceof MetaDataEntry || state instanceof MetaDataEntry[]) + { + return new BehaviorsMetaDataComponentState( + addBehaviors(component, null, behaviorsToAdd), state); + } + else + { + return addBehaviors(component, state, behaviorsToAdd); + } + } + + /** + * Construct a new component state with the given behaviors removed + * + * @param component + * the component to remove the behaviors from + * @param state + * the current component state + * @param modelSet + * a boolean indicating if the model is set + * @param behaviorsToRemove + * the behaviors to removed + * @return the new component state + */ + static Object removeBehaviors(Component component, Object state, boolean modelSet, + Behavior... behaviorsToRemove) + { + if (behaviorsToRemove.length == 0) + { + return state; + } + else if (state instanceof ComponentState) + { + ComponentState compState = (ComponentState) state; + return compState.setBehaviors( + removeBehaviors(component, compState.getBehaviors(), behaviorsToRemove)); + } + else if (modelSet) + { + throw cannotRemove(behaviorsToRemove[0]); + } + else if (state instanceof MetaDataEntry || state instanceof MetaDataEntry[]) + { + throw cannotRemove(behaviorsToRemove[0]); + } + else + { + return removeBehaviors(component, state, behaviorsToRemove); + } + } + + /** + * Construct a new component state with the behaviors replaced + * + * @param state + * the current component state + * @param modelSet + * a boolean indicating if the model is set + * @param behaviors + * the new value for the behaviors: null, one behavior or an array of behaviors + * @return the new component state + */ + static Object setBehaviors(Object state, boolean modelSet, Object behaviors) + { + if (state instanceof ComponentState) + { + ComponentState compState = (ComponentState) state; + return compState.setBehaviors(behaviors); + } + else if (state instanceof Behavior || state instanceof Behavior[] || state == null) + { + return behaviors; + } + else if (behaviors == null) + { + return state; + } + else if (modelSet) + { + return new ModelBehaviorsComponentState((IModel< ? >) state, behaviors); + } + else + { + return new BehaviorsMetaDataComponentState(behaviors, state); + } + } + + /** + * Construct a new component state with the given meta data entry set or reset + * + * @param state + * the current component state + * @param modelSet + * a boolean indicating if the model is set + * @param key + * the key to replace the value for + * @param data + * the new value for the meta data entry, null to clear + * @return the new component state + */ + static Object setMetaData(Object state, boolean modelSet, MetaDataKey key, T data) + { + if (state instanceof ComponentState) + { + ComponentState compState = (ComponentState) state; + return compState.setMetaData(setMetaData(compState.getMetaData(), key, data)); + } + else if (state instanceof MetaDataEntry || state instanceof MetaDataEntry[] + || state == null) + { + return setMetaData(state, key, data); + } + else if (data == null) + { + return state; + } + else if (modelSet) + { + return new ModelMetaDataComponentState((IModel< ? >) state, + new MetaDataEntry<>(key, data)); + } + else + { + return new BehaviorsMetaDataComponentState(state, new MetaDataEntry<>(key, data)); + } + } + + /** + * Bind a behavior to a component, adding a state change if needed. + * + * @param component + * @param behavior + */ + static void bindBehavior(Component component, Behavior behavior) + { + if (!behavior.isTemporary(component)) + { + component.addStateChange(); + } + behavior.bind(component); + } + + private static Object addBehaviors(Component component, Object behaviors, + Behavior... behaviorsToAdd) + { + // nothing to add + if (behaviorsToAdd.length == 0) + { + return behaviors; + } + + // the existing array is compact, adding cannot shrink it + int curLength = getBehaviorsLength(behaviors); + int newSize = Math.max(curLength, behaviorsToAdd.length + getBehaviorsLength(behaviors) + - getEmptyBehaviorsSlots(behaviors)); + + // new size is 1, it must be we are adding 1 to 0 + if (newSize == 1) + { + return behaviorsToAdd[0]; + } + + // construct the return array and copy existing behaviors + Behavior[] ret = new Behavior[newSize]; + if (behaviors instanceof Behavior[]) + { + System.arraycopy(behaviors, 0, ret, 0, curLength); + } + else + { + ret[0] = (Behavior) behaviors; + } + + // fill empty slots with behaviors to add + int checkSlot = 0; + for (Behavior behaviorToAdd : behaviorsToAdd) + { + while (ret[checkSlot] != null) + { + checkSlot++; + } + ret[checkSlot] = behaviorToAdd; + } + return ret; + } + + private static Object removeBehaviors(Component component, Object behaviors, + Behavior... behaviorsToRemove) + { + // nothing to remove + if (behaviorsToRemove.length == 0) + { + return behaviors; + } + if (behaviors == null) + { + throw cannotRemove(behaviorsToRemove[0]); + } + + if (behaviors instanceof Behavior) + { + if (!behaviorsToRemove[0].equals(behaviors)) + { + throw cannotRemove(behaviorsToRemove[0]); + } + if (behaviorsToRemove.length > 1) + { + throw cannotRemove(behaviorsToRemove[1]); + } + unbindBehavior(component, (Behavior) behaviors); + return null; + } + + Behavior[] behaviorArr = (Behavior[]) behaviors; + for (Behavior behaviorToRemove : behaviorsToRemove) + { + boolean found = false; + for (int i = 0; i < behaviorArr.length; i++) + { + Behavior curBehavior = behaviorArr[i]; + if (curBehavior != null && behaviorToRemove.equals(curBehavior)) + { + found = true; + unbindBehavior(component, curBehavior); + behaviorArr[i] = null; + break; + } + } + if (!found) + { + throw cannotRemove(behaviorToRemove); + } + } + return behaviorArr; + } + + private static IllegalStateException cannotRemove(Behavior behavior) + { + return new IllegalStateException( + "Tried to remove a behavior that was not added to the component. Behavior: " + + behavior.toString()); + } + + private static void unbindBehavior(Component component, Behavior behavior) + { + behavior.unbind(component); + if (!behavior.isTemporary(component)) + { + component.addStateChange(); + } + behavior.detach(component); + } + + private static int getBehaviorsLength(Object behaviors) + { + if (behaviors == null) + { + return 0; + } + return behaviors instanceof Behavior[] ? ((Behavior[]) behaviors).length : 1; + } + + private static int getEmptyBehaviorsSlots(Object behaviors) + { + if (!(behaviors instanceof Behavior[])) + { + return 0; + } + Behavior[] arr = (Behavior[]) behaviors; + int emptyCount = 0; + for (Behavior curBehavior : arr) + { + if (curBehavior == null) + { + emptyCount++; + } + } + return emptyCount; + } + + private static Object setMetaData(Object metadata, MetaDataKey key, T data) + { + if (metadata == null) + { + if (data == null) + { + return null; + } + else + { + return new MetaDataEntry<>(key, data); + } + } + else if (metadata instanceof MetaDataEntry) + { + MetaDataEntry< ? > curEntry = (MetaDataEntry< ? >) metadata; + if (curEntry.key.equals(key)) + { + if (data == null) + { + return null; + } + else + { + curEntry.object = data; + return curEntry; + } + } + else + { + if (data == null) + { + return metadata; + } + else + { + MetaDataEntry< ? >[] ret = new MetaDataEntry< ? >[2]; + ret[0] = (MetaDataEntry< ? >) metadata; + ret[1] = new MetaDataEntry<>(key, data); + return ret; + } + } + } + else + { + MetaDataEntry< ? >[] metadataArr = (MetaDataEntry< ? >[]) metadata; + for (int i = 0; i < metadataArr.length; i++) + { + MetaDataEntry< ? > curEntry = metadataArr[i]; + if (curEntry.key.equals(key)) + { + if (data == null) + { + if (metadataArr.length == 2) + { + return metadataArr[i == 0 ? 1 : 0]; + } + else + { + MetaDataEntry< ? >[] ret = + new MetaDataEntry< ? >[metadataArr.length - 1]; + System.arraycopy(metadataArr, 0, ret, 0, i); + System.arraycopy(metadataArr, i + 1, ret, i, ret.length - i); + return ret; + } + } + else + { + curEntry.object = data; + return metadataArr; + } + } + } + if (data == null) + { + return metadataArr; + } + MetaDataEntry< ? >[] ret = new MetaDataEntry< ? >[metadataArr.length + 1]; + System.arraycopy(metadataArr, 0, ret, 0, metadataArr.length); + ret[metadataArr.length] = new MetaDataEntry<>(key, data); + return ret; + } + } + + static Behavior getBehaviorById(Component component, int id, Object state, boolean modelSet) + { + Object behaviors = getBehaviors(state, modelSet); + if (behaviors instanceof Behavior) + { + if (id == 0) + { + return (Behavior) behaviors; + } + } + else if (behaviors instanceof Behavior[]) + { + Behavior[] behaviorsArr = (Behavior[]) behaviors; + if (behaviorsArr.length > id && behaviorsArr[id] != null) + { + return behaviorsArr[id]; + } + } + throw new InvalidBehaviorIdException(component, id); + } + + static int getBehaviorId(Component component, Behavior behavior, Object state, boolean modelSet) + { + Object behaviors = getBehaviors(state, modelSet); + if (behavior.equals(behaviors)) + { + return 0; + } + else if (behaviors instanceof Behavior[]) + { + Behavior[] behaviorsArr = (Behavior[]) behaviors; + for (int i = 0; i < behaviorsArr.length; i++) + { + if (behavior.equals(behaviorsArr[i])) + { + return i; + } + } + } + throw new IllegalStateException( + "Behavior must be added to component before its id can be generated. Behavior: " + + behavior + ", Component: " + component); + } + + @SuppressWarnings("unchecked") + static List getBehaviors(Class type, Object state, boolean modelSet) + { + Object behaviors = getBehaviors(state, modelSet); + if (behaviors == null) + { + return List.of(); + } + + if (behaviors instanceof Behavior) + { + if (type == null || type.isInstance(behaviors)) + { + return List.of((M) behaviors); + } + return List.of(); + } + + Behavior[] behaviorsArr = (Behavior[]) behaviors; + List subset = new ArrayList<>(behaviorsArr.length); + for (Behavior curBehavior : behaviorsArr) + { + if (curBehavior != null && (type == null || type.isInstance(curBehavior))) + { + subset.add((M) curBehavior); + } + } + if (subset.isEmpty()) + { + return List.of(); + } + return Collections.unmodifiableList(subset); + } + + static void onRemoveBehaviors(Component component, Object state, boolean modelSet) + { + Object behaviors = getBehaviors(state, modelSet); + if (behaviors instanceof Behavior) + { + ((Behavior) behaviors).onRemove(component); + } + else if (behaviors instanceof Behavior[]) + { + Behavior[] behaviorsArr = (Behavior[]) behaviors; + for (Behavior curBehavior : behaviorsArr) + { + if (curBehavior != null) + { + curBehavior.onRemove(component); + } + } + } + } + + static Object compactBehaviors(Component component, Object state, boolean modelSet, + boolean fixedIds) + { + if (fixedIds) + { + return state; + } + Object behaviors = getBehaviors(state, modelSet); + if (!(behaviors instanceof Behavior[])) + { + return state; + } + + Behavior[] behaviorsArr = (Behavior[]) behaviors; + int setIndex = 0; + int endIndex = behaviorsArr.length - 1; + int checkIndex = 0; + while (checkIndex <= endIndex) + { + Behavior curBehavior = behaviorsArr[checkIndex]; + if (curBehavior == null) + { + checkIndex++; + continue; + } + + // move tmp behaviors to the end of the array, swap with what's there + if (curBehavior.isTemporary(component)) + { + Behavior tmp = behaviorsArr[endIndex]; + behaviorsArr[endIndex] = curBehavior; + behaviorsArr[checkIndex] = tmp; + endIndex--; + continue; + } + behaviorsArr[setIndex] = curBehavior; + checkIndex++; + setIndex++; + } + + // wipe the remainder of the array + for (; setIndex <= endIndex; setIndex++) + { + behaviorsArr[setIndex] = null; + } + return state; + } + + static Object detachBehaviors(Component component, Object state, boolean modelSet, + boolean fixedIds) + { + Object behaviors = getBehaviors(state, modelSet); + if (behaviors instanceof Behavior) + { + Behavior behavior = (Behavior) behaviors; + behavior.detach(component); + if (behavior.isTemporary(component)) + { + behavior.unbind(component); + return setBehaviors(state, modelSet, null); + } + } + else if (behaviors instanceof Behavior[]) + { + // remove temporary behaviors and compact the array + int highestId = -1; + int filledSlots = 0; + Behavior[] behaviorsArr = (Behavior[]) behaviors; + + // iterate over all behaviors, detaching them and removing temporary behaviors + // remaining behaviors are counted and for stateful behaviors slots assigned + for (int i = 0; i < behaviorsArr.length; i++) + { + Behavior curBehavior = behaviorsArr[i]; + if (curBehavior != null) + { + curBehavior.detach(component); + if (curBehavior.isTemporary(component)) + { + curBehavior.unbind(component); + behaviorsArr[i] = null; + } + else + { + filledSlots++; + highestId = i; + } + } + } + + // if at most 1 behavior remains, no array is needed + int newSize = fixedIds ? Math.max(highestId + 1, filledSlots) : filledSlots; + if (newSize == 0) + { + return setBehaviors(state, modelSet, null); + } + if (newSize == 1) + { + return setBehaviors(state, modelSet, behaviorsArr[highestId]); + } + + // the calculated size is equal to the current size, cannot compact + if (newSize == behaviorsArr.length) + { + return state; + } + + // multiple behaviors (or one with an id > 0) + // construct a new array and compact the behaviors + Behavior[] ret = new Behavior[newSize]; + + if (fixedIds) + { + System.arraycopy(behaviorsArr, 0, ret, 0, ret.length); + } + else + { + int targetIndex = 0; + for (int i = 0; i < behaviorsArr.length; i++) + { + Behavior curBehavior = behaviorsArr[i]; + if (curBehavior == null) + { + continue; + } + ret[targetIndex] = curBehavior; + targetIndex++; + } + } + return setBehaviors(state, modelSet, ret); + } + return state; + } +} diff --git a/wicket-core/src/main/java/org/apache/wicket/ajax/AbstractDefaultAjaxBehavior.java b/wicket-core/src/main/java/org/apache/wicket/ajax/AbstractDefaultAjaxBehavior.java index 291303f6f18..76c4fb4ce09 100644 --- a/wicket-core/src/main/java/org/apache/wicket/ajax/AbstractDefaultAjaxBehavior.java +++ b/wicket-core/src/main/java/org/apache/wicket/ajax/AbstractDefaultAjaxBehavior.java @@ -83,12 +83,6 @@ protected void onBind() final Component component = getComponent(); component.setOutputMarkupId(true); - - if (getStatelessHint(component)) - { - //generate behavior id - component.getBehaviorId(this); - } } /** diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/lang/WicketObjects.java b/wicket-core/src/main/java/org/apache/wicket/core/util/lang/WicketObjects.java index 46491500fb4..f3b0b9573c8 100644 --- a/wicket-core/src/main/java/org/apache/wicket/core/util/lang/WicketObjects.java +++ b/wicket-core/src/main/java/org/apache/wicket/core/util/lang/WicketObjects.java @@ -17,6 +17,7 @@ package org.apache.wicket.core.util.lang; import java.io.Serializable; +import java.nio.file.Files; import org.apache.wicket.Application; import org.apache.wicket.Component; diff --git a/wicket-core/src/test/java/org/apache/wicket/behavior/ImmutableBehaviorIdsTest.java b/wicket-core/src/test/java/org/apache/wicket/behavior/ImmutableBehaviorIdsTest.java index 4f161ab0fa2..98e9a44c97b 100644 --- a/wicket-core/src/test/java/org/apache/wicket/behavior/ImmutableBehaviorIdsTest.java +++ b/wicket-core/src/test/java/org/apache/wicket/behavior/ImmutableBehaviorIdsTest.java @@ -75,10 +75,10 @@ void urlIndexRendering() assertTrue(output.contains("autocomplete=\"off\"")); assertTrue(output.contains("class2=\"border\"")); assertTrue(output.contains("autocomplete2=\"off\"")); - assertTrue(output.contains(".0")); - assertTrue(output.contains(".1")); - assertEquals(link, page.getContainer().getBehaviorById(0)); - assertEquals(link2, page.getContainer().getBehaviorById(1)); + assertTrue(output.contains(".2")); + assertTrue(output.contains(".4")); + assertEquals(link, page.getContainer().getBehaviorById(2)); + assertEquals(link2, page.getContainer().getBehaviorById(4)); // if we remove a behavior that is before the ibehaviorlistener its url index should not // change @@ -89,11 +89,11 @@ void urlIndexRendering() page.getContainer().remove(auto2); tester.startPage(page); output = tester.getLastResponseAsString(); - // System.out.println(output); - assertTrue(output.contains(".0")); - assertTrue(output.contains(".1")); - assertEquals(link, page.getContainer().getBehaviorById(0)); - assertEquals(link2, page.getContainer().getBehaviorById(1)); +// System.out.println(output); + assertTrue(output.contains(".2")); + assertTrue(output.contains(".4")); + assertEquals(link, page.getContainer().getBehaviorById(2)); + assertEquals(link2, page.getContainer().getBehaviorById(4)); } /** diff --git a/wicket-core/src/test/java/org/apache/wicket/markup/html/basic/SimplePageExpectedResult_13.html b/wicket-core/src/test/java/org/apache/wicket/markup/html/basic/SimplePageExpectedResult_13.html index c0ae3c90e23..e019755db78 100644 --- a/wicket-core/src/test/java/org/apache/wicket/markup/html/basic/SimplePageExpectedResult_13.html +++ b/wicket-core/src/test/java/org/apache/wicket/markup/html/basic/SimplePageExpectedResult_13.html @@ -18,7 +18,7 @@