/*
This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2014 Servoy BV
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation; either version 3 of the License, or (at your option) any
later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along
with this program; if not, see http://www.gnu.org/licenses or write to the Free
Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
*/
package com.servoy.j2db.server.ngclient;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.sablo.specification.PropertyDescription;
import org.sablo.specification.WebComponentSpecProvider;
import org.sablo.specification.WebObjectSpecification;
import org.sablo.specification.WebServiceSpecProvider;
import org.sablo.specification.property.CustomJSONArrayType;
import com.servoy.j2db.AbstractActiveSolutionHandler;
import com.servoy.j2db.FlattenedSolution;
import com.servoy.j2db.persistence.BaseComponent;
import com.servoy.j2db.persistence.Form;
import com.servoy.j2db.persistence.GraphicalComponent;
import com.servoy.j2db.persistence.IAnchorConstants;
import com.servoy.j2db.persistence.IBasicWebComponent;
import com.servoy.j2db.persistence.IFormElement;
import com.servoy.j2db.persistence.IPersist;
import com.servoy.j2db.persistence.IRepository;
import com.servoy.j2db.persistence.ISupportScrollbars;
import com.servoy.j2db.persistence.ISupportTabSeq;
import com.servoy.j2db.persistence.Part;
import com.servoy.j2db.persistence.PositionComparator;
import com.servoy.j2db.persistence.StaticContentSpecLoader;
import com.servoy.j2db.persistence.TabSeqComparator;
import com.servoy.j2db.server.ngclient.property.ComponentPropertyType;
import com.servoy.j2db.server.ngclient.property.types.NGTabSeqPropertyType;
import com.servoy.j2db.server.ngclient.property.types.PropertyPath;
import com.servoy.j2db.server.ngclient.template.FormTemplateGenerator;
import com.servoy.j2db.server.shared.ApplicationServerRegistry;
import com.servoy.j2db.server.shared.IApplicationServer;
import com.servoy.j2db.util.Debug;
import com.servoy.j2db.util.SortedList;
import com.servoy.j2db.util.UUID;
import com.servoy.j2db.util.Utils;
/**
* Class used to cache FormElements that can be cached.
* Also contains more code that is useful when working with FormElements.
*
* @author acostescu
*/
public class FormElementHelper
{
public final static FormElementHelper INSTANCE = new FormElementHelper();
// todo identity key? SolutionModel persist shouldn't be cached at all?
private final ConcurrentMap<String, FlattenedSolution> globalFlattendSolutions = new ConcurrentHashMap<>();
private final ConcurrentMap<IPersist, FormElement> persistWrappers = new ConcurrentHashMap<>();
private final ConcurrentMap<UUID, Map<TabSeqProperty, Integer>> formTabSequences = new ConcurrentHashMap<>();
public List<FormElement> getFormElements(Iterator<IPersist> iterator, IServoyDataConverterContext context)
{
List<FormElement> lst = new ArrayList<>();
while (iterator.hasNext())
{
IPersist persist = iterator.next();
if (persist instanceof IFormElement)
{
lst.add(getFormElement((IFormElement)persist, context, null));
}
}
return lst;
}
public FormElement getFormElement(IFormElement formElement, IServoyDataConverterContext context, PropertyPath propertyPath)
{
return getFormElement(formElement, context.getSolution(), propertyPath, (context.getApplication() != null && context.getApplication().isInDesigner()));
}
public FormElement getFormElement(IFormElement formElement, FlattenedSolution fs, PropertyPath propertyPath, final boolean designer)
{
// dont cache if solution model is used (media,valuelist,relations can be changed for a none changed element)
if (designer || (fs.getSolutionCopy(false) != null))
{
if (propertyPath == null)
{
propertyPath = new PropertyPath();
propertyPath.setShouldAddElementName();
}
if (formElement instanceof BodyPortal) return createBodyPortalFormElement((BodyPortal)formElement, fs, designer);
else return new FormElement(formElement, fs, propertyPath, designer);
}
FormElement persistWrapper = persistWrappers.get(formElement);
if (persistWrapper == null)
{
if (propertyPath == null)
{
propertyPath = new PropertyPath();
propertyPath.setShouldAddElementName();
}
if (formElement instanceof BodyPortal)
persistWrapper = createBodyPortalFormElement((BodyPortal)formElement, getSharedFlattenedSolution(fs), designer);
else persistWrapper = new FormElement(formElement, getSharedFlattenedSolution(fs), propertyPath, false);
FormElement existing = persistWrappers.putIfAbsent(formElement, persistWrapper);
if (existing != null)
{
persistWrapper = existing;
}
}
return persistWrapper;
}
private FlattenedSolution getSharedFlattenedSolution(FlattenedSolution fs)
{
FlattenedSolution flattenedSolution = globalFlattendSolutions.get(fs.getName());
if (flattenedSolution == null)
{
try
{
flattenedSolution = new FlattenedSolution(true);
flattenedSolution.setSolution(fs.getMainSolutionMetaData(), false, true,
new AbstractActiveSolutionHandler(ApplicationServerRegistry.getService(IApplicationServer.class))
{
@Override
public IRepository getRepository()
{
return ApplicationServerRegistry.get().getLocalRepository();
}
});
FlattenedSolution alreadyCreated = globalFlattendSolutions.putIfAbsent(flattenedSolution.getName(), flattenedSolution);
if (alreadyCreated != null)
{
flattenedSolution.close(null);
flattenedSolution = alreadyCreated;
}
}
catch (Exception e)
{
throw new RuntimeException("Can't create FlattenedSolution for: " + fs, e);
}
}
return flattenedSolution;
}
private FormElement createBodyPortalFormElement(BodyPortal listViewPortal, FlattenedSolution fs, final boolean isInDesigner)
{
Form form = listViewPortal.getForm();
Part bodyPart = null;
for (Part prt : Utils.iterate(form.getParts()))
{
if (prt.getPartType() == Part.BODY)
{
bodyPart = prt;
break;
}
}
if (bodyPart != null)
{
try
{
String name = "svy_lvp_" + form.getName();
int startPos = form.getPartStartYPos(bodyPart.getID());
int endPos = bodyPart.getHeight();
int bodyheight = endPos - startPos;
JSONObject portal = new JSONObject();
portal.put("name", name);
portal.put("multiLine", !listViewPortal.isTableview());
portal.put("rowHeight", !listViewPortal.isTableview() ? bodyheight : getRowHeight(form));
portal.put("scrollbars", form.getScrollbars());
if (listViewPortal.isTableview())
{
int headerHeight = 30;
if (form.hasPart(Part.HEADER))
{
headerHeight = 0;
}
portal.put("headerHeight", headerHeight);
portal.put("sortable", form.getOnSortCmdMethodID() != -1);
}
portal.put("readOnlyMode", form.getNgReadOnlyMode());
JSONObject location = new JSONObject();
location.put("x", 0);
location.put("y", isInDesigner ? startPos : 0);
portal.put("location", location);
JSONObject size = new JSONObject();
// size.put("width", (listViewPortal.isTableview() && !fillsWidth) ? getGridWidth(form) : form.getWidth());
size.put("width", form.getWidth());
size.put("height", bodyheight);
portal.put("size", size);
portal.put("visible", listViewPortal.getVisible());
portal.put("enabled", listViewPortal.getEnabled());
portal.put("childElements", new JSONArray()); // empty contents; will be updated afterwards directly with form element values for components
JSONObject relatedFoundset = new JSONObject();
relatedFoundset.put("foundsetSelector", "");
portal.put("relatedFoundset", relatedFoundset);
PropertyPath propertyPath = new PropertyPath();
propertyPath.setShouldAddElementName();
FormElement portalFormElement = new FormElement("servoycore-portal", portal, form, name, fs, propertyPath, isInDesigner);
PropertyDescription pd = portalFormElement.getWebComponentSpec().getProperties().get("childElements");
if (pd != null) pd = ((CustomJSONArrayType< ? , ? >)pd.getType()).getCustomJSONTypeDefinition();
if (pd == null)
{
Debug.error(new RuntimeException("Cannot find component definition special type to use for portal."));
return null;
}
ComponentPropertyType type = ((ComponentPropertyType)pd.getType());
Map<String, Object> portalFormElementProperties = new HashMap<>(portalFormElement.getRawPropertyValues());
portalFormElementProperties.put("anchors", IAnchorConstants.ALL);
portalFormElementProperties.put("offsetY", startPos);
portalFormElementProperties.put("partHeight", bodyPart.getHeight());
portalFormElementProperties.put("formview", true);
// now put real child component form element values in "childElements"
Iterator<IPersist> it = form.getAllObjects(PositionComparator.XY_PERSIST_COMPARATOR);
List<Object> children = new ArrayList<>(); // contains actually ComponentTypeFormElementValue objects
List<IPersist> labelFors = new ArrayList<>();
propertyPath.add(portalFormElement.getName());
propertyPath.add("childElements");
// it's a generated table-view form portal (BodyPortal); we just
// have to set the Portal's tabSeq to the first one of it's children for it to work properly
int minBodyPortalTabSeq = -2;
while (it.hasNext())
{
IPersist persist = it.next();
if (persist instanceof IFormElement)
{
Point loc = ((IFormElement)persist).getLocation();
if (startPos <= loc.y && endPos > loc.y)
{
if (listViewPortal.isTableview() && persist instanceof GraphicalComponent && ((GraphicalComponent)persist).getLabelFor() != null)
continue;
propertyPath.add(children.size());
FormElement fe = getFormElement((IFormElement)persist, fs, propertyPath, isInDesigner);
if (listViewPortal.isTableview())
{
String elementName = fe.getName();
Iterator<GraphicalComponent> graphicalComponents = form.getGraphicalComponents();
boolean hasLabelFor = false;
while (graphicalComponents.hasNext())
{
GraphicalComponent gc = graphicalComponents.next();
if (gc.getLabelFor() != null && Utils.equalObjects(elementName, gc.getLabelFor()) && startPos <= gc.getLocation().y &&
endPos > gc.getLocation().y)
{
labelFors.add(gc);
hasLabelFor = true;
break;
}
}
Map<String, Object> feRawProperties = new HashMap<>(fe.getRawPropertyValues());
feRawProperties.put("componentIndex", Integer.valueOf(children.size()));
if (hasLabelFor) feRawProperties.put("headerIndex", Integer.valueOf(labelFors.size() - 1));
fe.updatePropertyValuesDontUse(feRawProperties);
}
children.add(type.getFormElementValue(null, pd, propertyPath, fe, fs));
propertyPath.backOneLevel();
Collection<PropertyDescription> tabSequenceProperties = fe.getWebComponentSpec().getProperties(NGTabSeqPropertyType.NG_INSTANCE);
for (PropertyDescription tabSeqProperty : tabSequenceProperties)
{
String tabSeqPropertyName = tabSeqProperty.getName();
Integer tabSeqVal = (Integer)fe.getPropertyValue(tabSeqPropertyName);
if (tabSeqVal == null) tabSeqVal = Integer.valueOf(0); // default is 0 == DEFAULT tab sequence
if (minBodyPortalTabSeq < 0 || (minBodyPortalTabSeq > tabSeqVal.intValue() && tabSeqVal.intValue() >= 0))
minBodyPortalTabSeq = tabSeqVal.intValue();
}
}
}
}
propertyPath.backOneLevel();
propertyPath.backOneLevel();
portalFormElementProperties.put("childElements", children.toArray());
if (listViewPortal.isTableview())
{
propertyPath.add("headers");
List<Object> headers = new ArrayList<>();
for (IPersist persist : labelFors)
{
if (persist instanceof IFormElement)
{
propertyPath.add(headers.size());
FormElement fe = getFormElement((IFormElement)persist, fs, propertyPath, isInDesigner);
headers.add(type.getFormElementValue(null, pd, propertyPath, fe, fs));
propertyPath.backOneLevel();
}
}
propertyPath.backOneLevel();
propertyPath.backOneLevel();
portalFormElementProperties.put("headers", headers.toArray());
}
portalFormElementProperties.put("tabSeq", Integer.valueOf(minBodyPortalTabSeq)); // table view tab seq. is the minimum of it's children tabSeq'es
portalFormElement.updatePropertyValuesDontUse(portalFormElementProperties);
return portalFormElement;
}
catch (JSONException ex)
{
Debug.error("Cannot create list view portal component", ex);
}
}
return null;
}
private boolean fillsWidth(Form form)
{
if ((form.getScrollbars() & ISupportScrollbars.HORIZONTAL_SCROLLBAR_NEVER) == ISupportScrollbars.HORIZONTAL_SCROLLBAR_NEVER)
{
Part part = getBodyPart(form);
int startPos = form.getPartStartYPos(part.getID());
int endPos = part.getHeight();
Iterator<IPersist> it = form.getAllObjects(PositionComparator.XY_PERSIST_COMPARATOR);
while (it.hasNext())
{
IPersist persist = it.next();
if (persist instanceof GraphicalComponent && ((GraphicalComponent)persist).getLabelFor() != null) continue;
if (persist instanceof BaseComponent)
{
BaseComponent bc = (BaseComponent)persist;
if ((bc.getAnchors() & (IAnchorConstants.WEST + IAnchorConstants.EAST)) == (IAnchorConstants.WEST + IAnchorConstants.EAST))
{
return true;
}
}
}
}
return false;
}
public Part getBodyPart(Form form)
{
for (Part prt : Utils.iterate(form.getParts()))
{
if (prt.getPartType() == Part.BODY)
{
return prt;
}
}
return null;
}
public boolean hasExtraParts(Form form)
{
int count = 0;
for (Part prt : Utils.iterate(form.getParts()))
{
count++;
if (count >= 2)
{
return true;
}
}
return false;
}
private int getRowHeight(Form form)
{
int rowHeight = 0;
Part part = getBodyPart(form);
int startPos = form.getPartStartYPos(part.getID());
int endPos = part.getHeight();
Iterator<IPersist> it = form.getAllObjects(PositionComparator.XY_PERSIST_COMPARATOR);
while (it.hasNext())
{
IPersist persist = it.next();
if (persist instanceof GraphicalComponent && ((GraphicalComponent)persist).getLabelFor() != null) continue;
if (persist instanceof BaseComponent)
{
BaseComponent bc = (BaseComponent)persist;
Point location = bc.getLocation();
if (startPos <= location.y && endPos > location.y)
{
if (rowHeight == 0)
{
rowHeight = bc.getSize().height;
break;
}
}
}
}
return rowHeight == 0 ? 20 : rowHeight;
}
public void reload()
{
persistWrappers.clear();
formTabSequences.clear();
for (FlattenedSolution fs : globalFlattendSolutions.values())
{
fs.close(null);
}
globalFlattendSolutions.clear();
WebComponentSpecProvider.reload();
WebServiceSpecProvider.reload();
}
public void flush(Collection<IPersist> changes)
{
if (changes != null)
{
for (IPersist persist : changes)
{
persistWrappers.remove(persist);
}
}
}
/**
* Generates a Servoy controlled tab-sequence-index. We try to avoid sending default (0 or null) tabSeq even
* for forms that do use default tab sequence in order to avoid problems with nesting default and non-default tabSeq forms.
*
* @param designValue the value the persist holds for the tabSeq.
* @param form the form containing the persist
* @param persistIfAvailable the persist. For now, 'component' type properties might work with non-persist-linked FormElements so it could be null.
* When those become persist based as well this will never be null.
* @return the requested controlled tabSeq (should make tabSeq be identical to the one shown in developer).
*/
public Integer getControlledTabSeqReplacementFor(Integer designValue, PropertyDescription pd, Form flattenedForm, IPersist persistIfAvailable,
FlattenedSolution flattenedSolution) // TODO more args will be needed here such as the tabSeq property name or description
{
if (persistIfAvailable == null) return designValue; // TODO this can be removed when we know we'll always have a persist here; currently don't handle this in any way as it's not supported
boolean formWasModifiedViaSolutionModel = flattenedSolution.hasCopy(flattenedForm);
Map<TabSeqProperty, Integer> cachedTabSeq;
if (formWasModifiedViaSolutionModel) cachedTabSeq = null;
else cachedTabSeq = formTabSequences.get(flattenedForm.getUUID());
if (cachedTabSeq == null)
{
cachedTabSeq = new HashMap<TabSeqProperty, Integer>();
SortedList<TabSeqProperty> selected = new SortedList<TabSeqProperty>(new Comparator<TabSeqProperty>()
{
public int compare(TabSeqProperty o1, TabSeqProperty o2)
{
return TabSeqComparator.compareTabSeq(o1.getSeqValue(), o1.element, o2.getSeqValue(), o2.element);
}
});
Iterator<IFormElement> iterator = flattenedForm.getFlattenedObjects(null).iterator();
while (iterator.hasNext())
{
IFormElement formElement = iterator.next();
if (FormTemplateGenerator.isWebcomponentBean(formElement))
{
String componentType = FormTemplateGenerator.getComponentTypeName(formElement);
WebObjectSpecification specification = WebComponentSpecProvider.getInstance().getWebComponentSpecification(componentType);
if (specification != null)
{
Collection<PropertyDescription> properties = specification.getProperties(NGTabSeqPropertyType.NG_INSTANCE);
if (properties != null && properties.size() > 0)
{
IBasicWebComponent webComponent = (IBasicWebComponent)formElement;
for (PropertyDescription tabSeqProperty : properties)
{
int tabseq = Utils.getAsInteger(webComponent.getProperty(tabSeqProperty.getName()));
if (tabseq >= 0)
{
selected.add(new TabSeqProperty(formElement, tabSeqProperty.getName()));
}
else
{
cachedTabSeq.put(new TabSeqProperty(formElement, tabSeqProperty.getName()), Integer.valueOf(-2));
}
}
}
}
}
else if (formElement instanceof ISupportTabSeq)
{
if (((ISupportTabSeq)formElement).getTabSeq() >= 0)
{
selected.add(new TabSeqProperty(formElement, StaticContentSpecLoader.PROPERTY_TABSEQ.getPropertyName()));
}
else
{
cachedTabSeq.put(new TabSeqProperty(formElement, StaticContentSpecLoader.PROPERTY_TABSEQ.getPropertyName()), Integer.valueOf(-2));
}
}
}
int i = 1;
for (TabSeqProperty tabSeq : selected)
{
cachedTabSeq.put(tabSeq, Integer.valueOf(i++));
}
if (!formWasModifiedViaSolutionModel)
{
formTabSequences.putIfAbsent(flattenedForm.getUUID(), cachedTabSeq);
}
}
Integer controlledTabSeq = cachedTabSeq.get(new TabSeqProperty(flattenedForm.findChild(persistIfAvailable.getUUID()), pd.getName()));
if (controlledTabSeq == null) controlledTabSeq = Integer.valueOf(-2); // if not in tabSeq, use "skip" value
return controlledTabSeq;
}
public static class TabSeqProperty
{
public IFormElement element;
public String propertyName;
public TabSeqProperty(IFormElement element, String propertyName)
{
this.element = element;
this.propertyName = propertyName;
}
public int getSeqValue()
{
if (propertyName != null && element instanceof IBasicWebComponent)
{
String componentType = FormTemplateGenerator.getComponentTypeName(element);
WebObjectSpecification specification = WebComponentSpecProvider.getInstance().getWebComponentSpecification(componentType);
if (specification != null)
{
PropertyDescription property = specification.getProperty(propertyName);
if (property != null)
{
return Utils.getAsInteger(((IBasicWebComponent)element).getProperty(propertyName));
}
}
}
else if (element instanceof ISupportTabSeq)
{
return ((ISupportTabSeq)element).getTabSeq();
}
return -1;
}
@Override
public String toString()
{
return element.toString() + (propertyName != null ? " [" + propertyName + "]" : "");
}
@Override
public boolean equals(Object obj)
{
if (!(obj instanceof TabSeqProperty)) return false;
return Utils.equalObjects(element, ((TabSeqProperty)obj).element) && Utils.equalObjects(propertyName, ((TabSeqProperty)obj).propertyName);
}
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + ((propertyName == null) ? 0 : propertyName.hashCode());
result = prime * result + ((element == null) ? 0 : element.getID());
return result;
}
}
}