From bdf47068e9e671037954263f76935f5a3455e609 Mon Sep 17 00:00:00 2001 From: Emond Papegaaij Date: Thu, 30 Apr 2020 13:40:45 +0200 Subject: [PATCH 1/8] WICKET-6774: rework of component state State of a component is no longer stored as an array but using instances of ComponentState. These have a smaller memory footprint in most cases, are more efficient and the code is easier to read (I hope). Note that a small change in behavior is introduced: behavior ids are only maintained for statefull behaviors. Ids can change for other behaviors, also when combined on the same component. --- .../java/org/apache/wicket/Behaviors.java | 325 -------- .../java/org/apache/wicket/Component.java | 284 +------ .../org/apache/wicket/ComponentState.java | 714 ++++++++++++++++++ .../wicket/core/util/lang/WicketObjects.java | 1 + .../behavior/ImmutableBehaviorIdsTest.java | 26 +- .../basic/SimplePageExpectedResult_13.html | 2 +- 6 files changed, 759 insertions(+), 593 deletions(-) delete mode 100644 wicket-core/src/main/java/org/apache/wicket/Behaviors.java create mode 100644 wicket-core/src/main/java/org/apache/wicket/ComponentState.java 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 1fd5cbe9e11..9012253a5e4 100644 --- a/wicket-core/src/main/java/org/apache/wicket/Component.java +++ b/wicket-core/src/main/java/org/apache/wicket/Component.java @@ -381,8 +381,7 @@ 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; @@ -456,8 +455,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 +478,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 +929,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 +958,7 @@ public final void detach() detachModels(); // detach any behaviors - new Behaviors(this).detach(); + data = ComponentState.detachBehaviors(this, data, getFlag(FLAG_MODEL_SET)); } catch (Exception x) { @@ -1512,39 +1351,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 +2692,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 +2742,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 +2751,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 +3396,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 +4205,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,14 +4213,14 @@ public Component remove(final Behavior... behaviors) @Override public final Behavior getBehaviorById(int id) { - return new Behaviors(this).getBehaviorById(id); + return ComponentState.getBehaviorById(this, id, data, getFlag(FLAG_MODEL_SET)); } /** {@inheritDoc} */ @Override public final int getBehaviorId(Behavior behavior) { - return new Behaviors(this).getBehaviorId(behavior); + return ComponentState.getBehaviorId(this, behavior, data, getFlag(FLAG_MODEL_SET)); } /** @@ -4460,7 +4232,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..7795af750da --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/ComponentState.java @@ -0,0 +1,714 @@ +/* + * 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; + +abstract class ComponentState implements Serializable +{ + private static final long serialVersionUID = 1L; + + abstract IModel< ? > getModel(); + + abstract Object getBehaviors(); + + abstract Object getMetaData(); + + static class ModelBehaviorComponentState extends ComponentState + { + private static final long serialVersionUID = 1L; + + private IModel< ? > model; + + private Object behaviors; + + private ModelBehaviorComponentState(IModel< ? > model, Object behaviors) + { + this.model = model; + this.behaviors = behaviors; + } + + @Override + IModel< ? > getModel() + { + return model; + } + + @Override + Object getBehaviors() + { + return behaviors; + } + + @Override + Object getMetaData() + { + return null; + } + } + + 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 getBehaviors() + { + return null; + } + + @Override + Object getMetaData() + { + return metaData; + } + } + + 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 getBehaviors() + { + return behaviors; + } + + @Override + Object getMetaData() + { + return metaData; + } + } + + 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 getBehaviors() + { + return behaviors; + } + + @Override + Object getMetaData() + { + return metaData; + } + } + + static IModel< ? > getModel(Object state, boolean modelSet) + { + if (!modelSet || state == null) + { + return null; + } + if (state instanceof IModel) + { + return (IModel< ? >) state; + } + return ((ComponentState) state).getModel(); + } + + 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; + } + + 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; + } + + static Object setModel(IModel< ? > model, Object state, boolean modelSet) + { + return createState(model, getBehaviors(state, modelSet), getMetaData(state, modelSet)); + } + + static Object addBehaviors(Component component, Object state, boolean modelSet, + Behavior... behaviorsToAdd) + { + return createState(getModel(state, modelSet), + addBehaviors(component, getBehaviors(state, modelSet), behaviorsToAdd), + getMetaData(state, modelSet)); + } + + static Object removeBehaviors(Component component, Object state, boolean modelSet, + Behavior... behaviorsToRemove) + { + return createState(getModel(state, modelSet), + removeBehaviors(component, getBehaviors(state, modelSet), behaviorsToRemove), + getMetaData(state, modelSet)); + } + + static Object setMetaData(Object state, boolean modelSet, MetaDataKey key, T data) + { + return createState(getModel(state, modelSet), getBehaviors(state, modelSet), + setMetaData(getMetaData(state, modelSet), key, data)); + } + + private static Object createState(IModel< ? > model, Object behaviors, Object metaData) + { + if (model == null) + { + if (behaviors == null) + { + if (metaData == null) + { + return null; + } + else + { + return metaData; + } + } + else + { + if (metaData == null) + { + return behaviors; + } + else + { + return new BehaviorsMetaDataComponentState(behaviors, metaData); + } + } + } + else + { + if (behaviors == null) + { + if (metaData == null) + { + return model; + } + else + { + return new ModelMetaDataComponentState(model, metaData); + } + } + else + { + if (metaData == null) + { + return new ModelBehaviorComponentState(model, behaviors); + } + else + { + return new ModelBehaviorsMetaDataComponentState(model, behaviors, metaData); + } + } + } + } + + 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; + } + + static void bindBehavior(Component component, Behavior behavior) + { + if (!behavior.isTemporary(component)) + { + component.addStateChange(); + } + behavior.bind(component); + } + + 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 compactBehaviors(component, 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 Object compactBehaviors(Component component, Behavior[] behaviors) + { + // first find the number of behaviors and the highest statefull one + Behavior singleBehavior = null; + int highestId = -1; + int filledSlots = 0; + for (int i = 0; i < behaviors.length; i++) + { + Behavior curBehavior = behaviors[i]; + if (curBehavior != null) + { + singleBehavior = curBehavior; + filledSlots++; + if (!curBehavior.getStatelessHint(component)) + { + highestId = i; + } + } + } + + int newSize = Math.max(highestId + 1, filledSlots); + if (newSize == 0) + { + return null; + } + if (newSize == 1) + { + return singleBehavior; + } + + // multiple behaviors (or one with an id > 0) + Behavior[] ret = new Behavior[newSize]; + int checkSlot = 0; + for (int i = 0; i < behaviors.length; i++) + { + Behavior curBehavior = behaviors[i]; + if (curBehavior == null) + { + continue; + } + // statefull behaviors stay at their index + if (!curBehavior.getStatelessHint(component)) + { + ret[i] = curBehavior; + } + else + { + // for all others, find the first free slot + while (ret[checkSlot] != null) + { + checkSlot++; + } + ret[checkSlot] = curBehavior; + } + } + return ret; + } + + 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 detachBehaviors(Component component, Object state, boolean modelSet) + { + Object behaviors = getBehaviors(state, modelSet); + if (behaviors instanceof Behavior) + { + Behavior behavior = (Behavior) behaviors; + behavior.detach(component); + if (behavior.isTemporary(component)) + { + behavior.unbind(component); + return createState(getModel(state, modelSet), null, getMetaData(state, modelSet)); + } + } + else if (behaviors instanceof Behavior[]) + { + boolean changed = false; + Behavior[] behaviorsArr = (Behavior[]) behaviors; + 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; + changed = true; + } + } + } + if (changed) + { + return createState(getModel(state, modelSet), + compactBehaviors(component, behaviorsArr), getMetaData(state, modelSet)); + } + } + return state; + } +} 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 7c22e4c2c5c..de15cfe3b3f 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 @@ -73,10 +73,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 @@ -89,10 +89,10 @@ void urlIndexRendering() 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)); + assertTrue(output.contains(".2")); + assertTrue(output.contains(".4")); + assertEquals(link, page.getContainer().getBehaviorById(2)); + assertEquals(link2, page.getContainer().getBehaviorById(4)); } /** @@ -128,29 +128,29 @@ void behaviorDataArrayCleanup() page.getContainer().remove(border); behaviors = page.getContainer().getBehaviors(); assertEquals(5, behaviors.size()); - assertEquals(autoId, page.container.getBehaviorId(auto)); +// assertEquals(autoId, page.container.getBehaviorId(auto)); assertEquals(link2Id, page.container.getBehaviorId(link2)); // auto,link,border2,link2,auto2 page.getContainer().remove(link); behaviors = page.getContainer().getBehaviors(); assertEquals(4, behaviors.size()); - assertEquals(autoId, page.container.getBehaviorId(auto)); +// assertEquals(autoId, page.container.getBehaviorId(auto)); assertEquals(link2Id, page.container.getBehaviorId(link2)); // auto,border2,link2,auto2 page.getContainer().remove(auto2); behaviors = page.getContainer().getBehaviors(); assertEquals(3, behaviors.size()); - assertEquals(autoId, page.container.getBehaviorId(auto)); +// assertEquals(autoId, page.container.getBehaviorId(auto)); assertEquals(link2Id, page.container.getBehaviorId(link2)); // auto,border2,link2 page.getContainer().remove(link2); // last IBehaviorListener behaviors = page.getContainer().getBehaviors(); assertEquals(2, behaviors.size()); - assertEquals(autoId, page.container.getBehaviorId(auto)); - assertEquals(border2Id, page.container.getBehaviorId(border2)); +// assertEquals(autoId, page.container.getBehaviorId(auto)); +// assertEquals(border2Id, page.container.getBehaviorId(border2)); } 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 8c7ef18d3ac..2500f04b480 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 @@