blob: 91db44348857af9c1371afcec1fd0a67202d0f81 [file] [log] [blame]
package org.apache.fulcrum.intake.model;
/*
* 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.
*/
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.apache.avalon.framework.logger.LogEnabled;
import org.apache.avalon.framework.logger.Logger;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.pool2.BaseKeyedPooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.fulcrum.intake.IntakeException;
import org.apache.fulcrum.intake.IntakeServiceFacade;
import org.apache.fulcrum.intake.Retrievable;
import org.apache.fulcrum.parser.ValueParser;
/**
* Holds a group of Fields
*
* @author <a href="mailto:jmcnally@collab.net">John McNally</a>
* @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
* @author <a href="mailto:quintonm@bellsouth.net">Quinton McCombs</a>
* @version $Id$
*/
@XmlType(name="group")
@XmlAccessorType(XmlAccessType.NONE)
public class Group implements Serializable, LogEnabled
{
/** Serial version */
private static final long serialVersionUID = -5452725641409669284L;
public static final String EMPTY = "";
/*
* An id representing a new object.
*/
public static final String NEW = "_0";
/** Logging */
private transient Logger log;
/**
* The key used to represent this group in a parameter.
* This key is usually a prefix as part of a field key.
*/
@XmlAttribute(name="key", required=true)
private String gid;
/**
* The name used in templates and java code to refer to this group.
*/
@XmlAttribute(required=true)
private String name;
/**
* The number of Groups with the same name that will be pooled.
*/
@XmlAttribute
private int poolCapacity = 128;
/**
* The default map object for this group
*/
@XmlAttribute(name="mapToObject")
private String defaultMapToObject;
/**
* The parent element in the XML tree
*/
private AppData parent;
/**
* A map of the fields in this group mapped by field name.
*/
private Map<String, Field<?>> fieldsByName;
/**
* Map of the fields by mapToObject
*/
private Map<String, Field<?>[]> mapToObjectFields;
/**
* A list of fields in this group.
*/
private LinkedList<Field<?>> fields;
/**
* The object id used to associate this group to a bean
* for one request cycle
*/
private String oid;
/**
* The object containing the request data
*/
private transient ValueParser pp;
/**
* A flag to help prevent duplicate hidden fields declaring this group.
*/
private boolean isDeclared;
/**
* Default constructor
*/
public Group()
{
super();
this.fields = new LinkedList<Field<?>>();
}
/**
* Enable Avalon Logging
*/
@Override
public void enableLogging(Logger logger)
{
this.log = logger.getChildLogger(getClass().getSimpleName());
}
/**
* Initializes the default Group using parameters.
*
* @param pp a <code>ValueParser</code> value
* @return this Group
* @throws IntakeException if at least one field could not be initialized
*/
public Group init(ValueParser pp) throws IntakeException
{
return init(NEW, pp);
}
/**
* Initializes the Group with parameters from RunData
* corresponding to key.
*
* @param key the group id
* @param pp a <code>ValueParser</code> value
* @return this Group
* @throws IntakeException if at least one field could not be initialized
*/
public Group init(String key, ValueParser pp) throws IntakeException
{
this.oid = key;
this.pp = pp;
for (ListIterator<Field<?>> i = fields.listIterator(fields.size()); i.hasPrevious();)
{
i.previous().init(pp);
}
for (ListIterator<Field<?>> i = fields.listIterator(fields.size()); i.hasPrevious();)
{
Field<?> field = i.previous();
if (field.isSet() && !field.isValidated())
{
field.validate();
}
}
return this;
}
/**
* Initializes the group with properties from an object.
*
* @param obj a <code>Persistent</code> value
* @return a <code>Group</code> value
*/
public Group init(Retrievable obj)
{
this.oid = obj.getQueryKey();
Class<?> cls = obj.getClass();
while (cls != null)
{
Field<?>[] flds = mapToObjectFields.get(cls.getName());
if (flds != null)
{
for (int i = flds.length - 1; i >= 0; i--)
{
flds[i].init(obj);
}
}
// Also check any interfaces
Class<?>[] interfaces = cls.getInterfaces();
for (int idx = 0; idx < interfaces.length; idx++)
{
Field<?>[] interfaceFields =
mapToObjectFields.get(interfaces[idx].getName());
if (interfaceFields != null)
{
for (int i = 0; i < interfaceFields.length; i++)
{
interfaceFields[i].init(obj);
}
}
}
cls = cls.getSuperclass();
}
return this;
}
/**
* Gets a list of the names of the fields stored in this object.
*
* @return A String array containing the list of names.
*/
public String[] getFieldNames()
{
String nameList[] = new String[fields.size()];
int i = 0;
for (Field<?> f : fields)
{
nameList[i++] = f.getName();
}
return nameList;
}
/**
* Return the name given to this group. The long name is to
* avoid conflicts with the get(String key) method.
*
* @return a <code>String</code> value
*/
public String getIntakeGroupName()
{
return name;
}
/**
* Get the number of Group objects that will be pooled.
*
* @return an <code>int</code> value
*/
public int getPoolCapacity()
{
return poolCapacity;
}
/**
* Get the part of the key used to specify the group.
* This is specified in the key attribute in the xml file.
*
* @return a <code>String</code> value
*/
public String getGID()
{
return gid;
}
/**
* Get the part of the key that distinguishes a group
* from others of the same name.
*
* @return a <code>String</code> value
*/
public String getOID()
{
return oid;
}
/**
* Concatenation of gid and oid.
*
* @return a <code>String</code> value
*/
public String getObjectKey()
{
return gid + oid;
}
/**
* Default object to map this group to.
*
* @return a <code>String</code> value
*/
public String getDefaultMapToObject()
{
return defaultMapToObject;
}
/**
* Describe <code>getObjects</code> method here.
*
* @param pp a <code>ValueParser</code> value
* @return an <code>ArrayList</code> value
* @throws IntakeException if an error occurs
*/
public List<Group> getObjects(ValueParser pp) throws IntakeException
{
ArrayList<Group> objs = null;
String[] oids = pp.getStrings(gid);
if (oids != null)
{
objs = new ArrayList<Group>(oids.length);
for (int i = oids.length - 1; i >= 0; i--)
{
objs.add(IntakeServiceFacade.getGroup(name).init(oids[i], pp));
}
}
return objs;
}
/**
* Get the Field
*
* @param fieldName the name of the field
* @return the named field
* @throws IntakeException indicates the field could not be found.
*/
public Field<?> get(String fieldName)
throws IntakeException
{
if (fieldsByName.containsKey(fieldName))
{
return fieldsByName.get(fieldName);
}
else
{
throw new IntakeException("Intake Field name: " + fieldName +
" not found in Group " + name);
}
}
/**
* Get the list of Fields.
* @return list of Fields
*/
public List<Field<?>> getFields()
{
return fields;
}
/**
* Set a collection of fields for this group
*
* @param inputFields the fields to set
*/
@XmlElement(name="field")
@XmlJavaTypeAdapter(FieldAdapter.class)
protected void setFields(List<Field<?>> inputFields)
{
fields = new LinkedList<Field<?>>(inputFields);
}
/**
* Performs an AND between all the fields in this group.
*
* @return a <code>boolean</code> value
*/
public boolean isAllValid()
{
boolean valid = true;
for (ListIterator<Field<?>> i = fields.listIterator(fields.size()); i.hasPrevious();)
{
Field<?> field = i.previous();
valid &= field.isValid();
if (log.isDebugEnabled() && !field.isValid())
{
log.debug("Group(" + oid + "): " + name + "; Field: "
+ field.getName() + "; value=" +
field.getValue() + " is invalid!");
}
}
return valid;
}
/**
* Calls a setter methods on obj, for fields which have been set.
*
* @param obj Object to be set with the values from the group.
* @throws IntakeException indicates that a failure occurred while
* executing the setter methods of the mapped object.
*/
public void setProperties(Object obj) throws IntakeException
{
Class<?> cls = obj.getClass();
while (cls != null)
{
if (log.isDebugEnabled())
{
log.debug("setProperties(" + cls.getName() + ")");
}
Field<?>[] flds = mapToObjectFields.get(cls.getName());
if (flds != null)
{
for (int i = flds.length - 1; i >= 0; i--)
{
flds[i].setProperty(obj);
}
}
// Also check any interfaces
Class<?>[] interfaces = cls.getInterfaces();
for (int idx = 0; idx < interfaces.length; idx++)
{
Field<?>[] interfaceFields =
mapToObjectFields.get(interfaces[idx].getName());
if (interfaceFields != null)
{
for (int i = 0; i < interfaceFields.length; i++)
{
interfaceFields[i].setProperty(obj);
}
}
}
cls = cls.getSuperclass();
}
log.debug("setProperties() finished");
}
/**
* Calls a setter methods on obj, for fields which pass validity tests.
* In most cases one should call Intake.isAllValid() and then if that
* test passes call setProperties. Use this method when some data is
* known to be invalid, but you still want to set the object properties
* that are valid.
*
* @param obj the object to set the properties for
*/
public void setValidProperties(Object obj)
{
Class<?> cls = obj.getClass();
while (cls != null)
{
Field<?>[] flds = mapToObjectFields.get(cls.getName());
if (flds != null)
{
for (int i = flds.length - 1; i >= 0; i--)
{
try
{
flds[i].setProperty(obj);
}
catch (IntakeException e)
{
// just move on to next field
}
}
}
// Also check any interfaces
Class<?>[] interfaces = cls.getInterfaces();
for (int idx = 0; idx < interfaces.length; idx++)
{
Field<?>[] interfaceFields =
mapToObjectFields.get(interfaces[idx].getName());
if (interfaceFields != null)
{
for (int i = 0; i < interfaceFields.length; i++)
{
try
{
interfaceFields[i].setProperty(obj);
}
catch(IntakeException e)
{
// just move on to next field
}
}
}
}
cls = cls.getSuperclass();
}
}
/**
* Calls getter methods on objects that are known to Intake
* so that field values in forms can be initialized from
* the values contained in the intake tool.
*
* @param obj Object that will be used to as a source of data for
* setting the values of the fields within the group.
* @throws IntakeException indicates that a failure occurred while
* executing the setter methods of the mapped object.
*/
public void getProperties(Object obj) throws IntakeException
{
Class<?> cls = obj.getClass();
while (cls != null)
{
Field<?>[] flds = mapToObjectFields.get(cls.getName());
if (flds != null)
{
for (int i = flds.length - 1; i >= 0; i--)
{
flds[i].getProperty(obj);
}
}
// Also check any interfaces
Class<?>[] interfaces = cls.getInterfaces();
for (int idx = 0; idx < interfaces.length; idx++)
{
Field<?>[] interfaceFields =
mapToObjectFields.get(interfaces[idx].getName());
if (interfaceFields != null)
{
for (int i = 0; i < interfaceFields.length; i++)
{
interfaceFields[i].getProperty(obj);
}
}
}
cls = cls.getSuperclass();
}
}
/**
* Removes references to this group and its fields from the
* query parameters
*/
public void removeFromRequest()
{
if (pp != null)
{
String[] groups = pp.getStrings(gid);
if (groups != null)
{
pp.remove(gid);
for (int i = 0; i < groups.length; i++)
{
if (groups[i] != null && !groups[i].equals(oid))
{
pp.add(gid, groups[i]);
}
}
for (ListIterator<Field<?>> i = fields.listIterator(fields.size()); i.hasPrevious();)
{
i.previous().removeFromRequest();
}
}
}
}
/**
* To be used in the event this group is used within multiple
* forms within the same template.
*/
public void resetDeclared()
{
isDeclared = false;
}
/**
* A xhtml valid hidden input field that notifies intake of the
* group's presence.
*
* @return a <code>String</code> value
*/
public String getHtmlFormInput()
{
StringBuilder sb = new StringBuilder(64);
appendHtmlFormInput(sb);
return sb.toString();
}
/**
* A xhtml valid hidden input field that notifies intake of the
* group's presence.
*
* @param sb the string builder to append the HTML to
*/
public void appendHtmlFormInput(StringBuilder sb)
{
if (!isDeclared)
{
isDeclared = true;
sb.append("<input type=\"hidden\" name=\"")
.append(gid)
.append("\" value=\"")
.append(oid)
.append("\"/>\n");
}
}
/**
* Creates a string representation of this input group. This
* is an xml representation.
*/
@Override
public String toString()
{
StringBuilder result = new StringBuilder();
result.append("<group name=\"").append(getIntakeGroupName()).append("\"");
result.append(" key=\"").append(getGID()).append("\"");
result.append(">\n");
if (fields != null)
{
for (Field<?> field : fields)
{
result.append(field);
}
}
result.append("</group>\n");
return result.toString();
}
/**
* Get the parent AppData for this group
*
* @return the parent
*/
public AppData getAppData()
{
return parent;
}
/**
* JAXB callback to set the parent object
*
* @param um the Unmarshaller
* @param parent the parent object (an AppData object)
*/
public void afterUnmarshal(Unmarshaller um, Object parent)
{
this.parent = (AppData)parent;
// Build map
fieldsByName = new HashMap<String, Field<?>>((int) (1.25 * fields.size() + 1));
for (Field<?> field : fields)
{
fieldsByName.put(field.getName(), field);
}
Map<String, List<Field<?>>> mapToObjectFieldLists =
new HashMap<String, List<Field<?>>>((int) (1.25 * fields.size() + 1));
// Fix fields
for (Field<?> field : fields)
{
if (StringUtils.isNotEmpty(field.mapToObject))
{
field.mapToObject = this.parent.getBasePackage() + field.mapToObject;
}
// map fields by their mapToObject
List<Field<?>> tmpFields = mapToObjectFieldLists.computeIfAbsent(
field.getMapToObject(),
k -> new ArrayList<Field<?>>(fields.size()));
tmpFields.add(field);
}
// Change the mapToObjectFields values to Field[]
mapToObjectFields = new HashMap<String, Field<?>[]>((int) (1.25 * fields.size() + 1));
for (Map.Entry<String, List<Field<?>>> entry : mapToObjectFieldLists.entrySet())
{
mapToObjectFields.put(entry.getKey(),
entry.getValue().toArray(new Field[entry.getValue().size()]));
}
}
// ********** PoolableObjectFactory implementation ******************
public static class GroupFactory
extends BaseKeyedPooledObjectFactory<String, Group>
{
private final AppData appData;
public GroupFactory(AppData appData)
{
this.appData = appData;
}
/**
* Creates an instance that can be returned by the pool.
* @param key the name of the group
* @return an instance that can be returned by the pool.
* @throws IntakeException indicates that the group could not be retrieved
*/
@Override
public Group create(String key) throws IntakeException
{
return appData.getGroup(key);
}
/**
* @see org.apache.commons.pool2.BaseKeyedPooledObjectFactory#wrap(java.lang.Object)
*/
@Override
public PooledObject<Group> wrap(Group group)
{
return new DefaultPooledObject<Group>(group);
}
/**
* Uninitialize an instance to be returned to the pool.
* @param key the name of the group
* @param pooledGroup the instance to be passivated
*/
@Override
public void passivateObject(String key, PooledObject<Group> pooledGroup)
{
Group group = pooledGroup.getObject();
group.oid = null;
group.pp = null;
for (ListIterator<Field<?>> i = group.fields.listIterator(group.fields.size());
i.hasPrevious();)
{
i.previous().dispose();
}
group.isDeclared = false;
}
}
}