/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of Business Objects nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* RecordCreationGem.java
* Creation date: July 27, 2007.
* By: Jennifer Chen
*/
package org.openquark.gems.client;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.openquark.cal.compiler.CompositionNode;
import org.openquark.cal.compiler.FieldName;
import org.openquark.cal.compiler.LanguageInfo;
import org.openquark.cal.compiler.TypeExpr;
import org.openquark.cal.metadata.ArgumentMetadata;
import org.openquark.cal.services.CALFeatureName;
import org.openquark.util.Pair;
import org.openquark.util.xml.BadXMLDocumentException;
import org.openquark.util.xml.XMLPersistenceHelper;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
/**
* An RecordCreatonGem is a Gem which creates a record.
* @author Jennifer Chen
*/
public class RecordCreationGem extends Gem implements CompositionNode.RecordCreationNode {
// Used for making a valid ordinal field name
private static final String DEFAULT_NEW_FIELD_PREFIX = "#";
private static final int DEFAULT_NUM_FIELDS = 2;
/** Used to suggest the new field and argument name to be created, needs to be checked for uniqueness */
private int nextPossibleFieldName = 1;
/** The current list of record fields in order of addition */
private List<String> fields = new ArrayList<String>();
/**
* Constructor. A default number of fields are created.
*/
public RecordCreationGem() {
super(0);
addNewFields(DEFAULT_NUM_FIELDS);
}
/**
* Add new fields to the end of the fields list with a default name then updates the inputs
* @param numFields the number of fields to add
*/
public void addNewFields(int numFields) {
// Save the state before adding new fields
Map<String, PartInput> fieldNameToInputMap = getFieldNameToInputMap();
for (int i = 0; i < numFields; i++) {
fields.add(getUniqueFieldName(fields));
nextPossibleFieldName++;
}
// Update the gem inputs
updateInputs(fieldNameToInputMap);
}
/**
* Delete the specified field
* @param fieldToDelete the field to delete from record
* @throws IllegalArgumentException
*/
public void deleteField(String fieldToDelete) {
// Save the state before deleting
Map<String, PartInput> fieldNameToInputMap = getFieldNameToInputMap();
if (fields.remove(fieldToDelete)) {
updateInputs(fieldNameToInputMap);
} else {
throw new IllegalArgumentException("The field to delete does not exist.");
}
}
/**
* Rename an existing field with a valid new field name which does not already exist. Renaming an field to
* its original name (essentially no change) is allowed
* @param fieldIndex the index of the field to rename
* @param newName the new field name
* @throws IllegalArgumentException if newName is null or newName is a duplicate of an existing name other than itself.
*/
public void renameRecordField(int fieldIndex, String newName) {
String oldName = fields.get(fieldIndex);
if (newName == null) {
throw new IllegalArgumentException("The field cannot be renamed if the new name is full");
}
fields.set(fieldIndex, newName);
PartInput input = this.getInputPart(fieldIndex);
String newInputName = getArgumentString(newName);
// this is the displayed name
input.setOriginalInputName(newInputName);
// but setting the argument name is necessary to change the display instantly.
input.setArgumentName(new ArgumentName(newInputName));
// Update the metadata as well. For now user-set names will be lost when renaming happens
// this line: input.getDesignMetadata().setDisplayName(newInputName); does not overwrite the existing metadata.
ArgumentMetadata metadata = new ArgumentMetadata(CALFeatureName.getArgumentFeatureName(fieldIndex), input.getDesignMetadata().getLocale());
metadata.setDisplayName(newInputName);
input.setDesignMetadata(metadata);
if (nameChangeListener != null) {
nameChangeListener.nameChanged(new NameChangeEvent(this, oldName));
}
}
/**
* Updates the input according the current list of fields.
* The updated inputs will be in the order of their corresponding fields.
*
* @param fieldnameToInputMap the map of field to input before the fields were updated,
* used to preserve the correct match up for fields and inputs
*/
void updateInputs(Map<String, PartInput> fieldnameToInputMap) {
int nNewInputs = fields.size();
PartInput[] newInputArray = new PartInput[nNewInputs];
for (int i = 0; i < nNewInputs; i++) {
String name = fields.get(i);
PartInput input = fieldnameToInputMap.get(name);
if (input != null) {
// existing input
input.setInputNum(i);
input.setOriginalInputName(getArgumentString(name));
} else {
// new input
input = createInputPart(i);
input.setType(TypeExpr.makeParametricType());
String argName = getArgumentString(name);
input.setArgumentName(new ArgumentName(argName));
input.setOriginalInputName(argName);
// Needs to set metadata because of locale
ArgumentMetadata metadata = new ArgumentMetadata(CALFeatureName.getArgumentFeatureName(i), input.getDesignMetadata().getLocale());
metadata.setDisplayName(argName);
input.setDesignMetadata(metadata);
}
newInputArray[i] = input;
}
setInputParts(newInputArray);
}
/**
* Get a list of deletable fields
* @param tabletop
* @return fields that can be deleted
*/
public List<String> getDeletableFields(TableTop tabletop) {
int nArgs = getNInputs();
List<String> deletableFields = new ArrayList<String>(nArgs);
// Get all the inputs that are not connected
for (int i = 0; i < nArgs; i++) {
PartInput input = this.getInputPart(i);
if (!input.isConnected()) {
deletableFields.add(fields.get(i));
}
}
return deletableFields;
}
/**
* Get a list of renamable field
* @param tabletop
* @return fields that can be renamed
*/
public List<String> getRenamableFields(TableTop tabletop) {
// For now return all fields are renamable
return this.getCopyOfFieldsList();
}
/**
* Generates a string for the argument display
* @param fieldName the field to generate a string for
* @return a string representing a field's corresponding input
*/
private String getArgumentString(String fieldName) {
StringBuilder fieldNameStr = new StringBuilder(fieldName);
if (fieldName.length() > 0 && fieldName.charAt(0) == '#') {
fieldNameStr.deleteCharAt(0);
}
return "fld_" + fieldNameStr;
}
/**
* Helper method to find an unique new field name
* @param hasFields the field names that already exists for this gem
* @return a new field name
*/
private String getUniqueFieldName(Collection<String> hasFields) {
for (int i = nextPossibleFieldName; true; i++) {
String name = DEFAULT_NEW_FIELD_PREFIX + i;
if (LanguageInfo.isValidFieldName(name) && !hasFields.contains(name)) {
return name;
}
}
}
/**
* Retrieve the most up-to-date fields and their corresponding PartInputs in order of the fields list
* @return mapping of field names to PartInputs
*/
public Map<String, PartInput> getFieldNameToInputMap() {
Map<String, PartInput> fieldToInputMap = new LinkedHashMap<String, PartInput>();
int nArgs = getNInputs();
for (int i = 0; i < nArgs; i++) {
PartInput input = getInputPart(i);
fieldToInputMap.put(fields.get(i), input);
}
return fieldToInputMap;
}
/**
* @return a copy of the fields list
*/
public List<String> getCopyOfFieldsList() {
List<String> newFieldsList = new ArrayList<String>(fields.size());
for (String fieldToCopy : fields) {
newFieldsList.add(fieldToCopy);
}
return newFieldsList;
}
/**
* Sets the fields list for this gem. Replaces all existing fields.
* Note: this does not update the gem's inputs
* @param newFields
*/
void setFieldNames(List<String> newFields) {
this.fields.clear();
fields.addAll(newFields);
}
/**
* Sets the next potential ordinal field name to be created
* @param num the next possible field name, should be > 0
*/
void setNextPossibleFieldName(int num) {
if (num > 0) {
this.nextPossibleFieldName = num;
}
}
/**
* Gets the NextPossibleFieldName which suggests the ordinal name for next field created
* @return int the integer part of the potential ordinal field name
*/
int getNextPossibleFieldName() {
return nextPossibleFieldName;
}
/**
* @param index the index of the field in the list
* @return the field name at the specified index in the field list
*/
public FieldName getFieldName(int index) {
return FieldName.make(fields.get(index));
}
/**
* @param field the field to retrieve index for
* @return the index of the field in question
*/
public int getFieldIndex(FieldName field) {
return fields.indexOf(field.getCalSourceForm());
}
/**
* @return the string representation of the field names
*/
public String getDisplayName() {
StringBuilder displayName = new StringBuilder("{ ");
boolean isFirstField = true;
for (String field : fields) {
if (isFirstField) {
isFirstField = false;
} else {
displayName.append(", ");
}
displayName.append(field);
}
displayName.append(" }");
return displayName.toString();
}
/*
* Methods supporting XMLPersistable ********************************************
*/
/**
* {@inheritDoc}
*/
@Override
public void saveXML(Node parentNode, GemContext gemContext) {
if (fields.isEmpty()) {
throw new IllegalStateException("RecordCreationGem cannot be saved while fields are empty");
}
Document document = (parentNode instanceof Document) ? (Document)parentNode : parentNode.getOwnerDocument();
// Create the RecordCreationGem element
Element resultElement = document.createElementNS(GemPersistenceConstants.GEM_NS, GemPersistenceConstants.RECORD_CREATION_GEM_TAG);
resultElement.setPrefix(GemPersistenceConstants.GEM_NS_PREFIX);
parentNode.appendChild(resultElement);
// Add info for the superclass gem.
super.saveXML(resultElement, gemContext);
// Now add RecordCreationGem specific info
// Element for the arguments
Element argumentsElement = document.createElement(GemPersistenceConstants.ARGUMENTS_TAG);
resultElement.appendChild(argumentsElement);
// Get and add the element for each argument
for (final PartInput input : this.getInputParts()) {
Element argumentElement = GemCutterPersistenceHelper.inputToArgumentElement(input, document, gemContext);
argumentsElement.appendChild(argumentElement);
}
// Element for the field names
Element fieldNamesElement = document.createElement(GemPersistenceConstants.RECORD_CREATION_GEM_FIELDS_TAG);
resultElement.appendChild(fieldNamesElement);
for (final String name : this.fields) {
XMLPersistenceHelper.addTextElement(fieldNamesElement, GemPersistenceConstants.RECORD_CREATION_GEM_FIELD_TAG, name);
}
resultElement.setAttribute(GemPersistenceConstants.RECORD_CREATION_GEM_NEXT_FIELDNAME_ATTR, String.valueOf(nextPossibleFieldName));
}
/**
* Create a new RecordCreationGem and loads its state from the specified XML element.
* @param gemElement Element the element representing the structure to deserialize.
* @param gemContext the context in which the gem is being instantiated.
* @param loadInfo the argument info for this load session.
* @return RecordCreationGem
* @throws BadXMLDocumentException
*/
public static RecordCreationGem getFromXML(Element gemElement, GemContext gemContext, Argument.LoadInfo loadInfo) throws BadXMLDocumentException {
RecordCreationGem gem = new RecordCreationGem();
gem.loadXML(gemElement, gemContext, loadInfo);
return gem;
}
/**
* Load this object's state.
* @param gemElement Element the element representing the structure to deserialize.
* @param gemContext the context in which the gem is being instantiated.
* @param loadInfo loadInfo for the arguments
*/
void loadXML(Element gemElement, GemContext gemContext, Argument.LoadInfo loadInfo) throws BadXMLDocumentException {
XMLPersistenceHelper.checkTag(gemElement, GemPersistenceConstants.RECORD_CREATION_GEM_TAG);
XMLPersistenceHelper.checkPrefix(gemElement, GemPersistenceConstants.GEM_NS_PREFIX);
List<Element> childElems = XMLPersistenceHelper.getChildElements(gemElement);
int nChildElems = childElems.size();
// Get info for the underlying gem.
Element superGemElem = (childElems.size() < 1) ? null : (Element)childElems.get(0);
XMLPersistenceHelper.checkIsElement(superGemElem);
super.loadXML(superGemElem, gemContext);
// Figure out the record node
// Get the arguments
Element argumentsElement = (nChildElems < 2) ? null : (Element)childElems.get(1);
XMLPersistenceHelper.checkIsElement(argumentsElement);
List<Element> argumentsChildElems = XMLPersistenceHelper.getChildElements(argumentsElement);
int nArgumentsChildElems = argumentsChildElems.size();
// Get the field names
Element fieldNamesElement = (nChildElems < 3) ? null : (Element)childElems.get(2);
XMLPersistenceHelper.checkIsElement(fieldNamesElement);
List<Element> fieldsChildElems = XMLPersistenceHelper.getChildElements(fieldNamesElement);
if (fieldsChildElems.size() != nArgumentsChildElems) {
throw new BadXMLDocumentException(null, "Number of fields and inputs do not correspond.");
}
// Erase the default fields
fields.clear();
for (int i = 0; i < nArgumentsChildElems; i++) {
Element inputChildElem = argumentsChildElems.get(i);
Pair<String, Integer> inputInfoPair = GemCutterPersistenceHelper.argumentElementToInputInfo(inputChildElem);
// Add the argument info the load info.
loadInfo.addArgument(this, inputInfoPair.fst(), inputInfoPair.snd(), null);
Element fieldChildElem = fieldsChildElems.get(i);
String name = XMLPersistenceHelper.getChildText(fieldChildElem);
// Update the fields list by appending the name to the end of the list
if (name != null) {
fields.add(name);
}
}
try {
nextPossibleFieldName = XMLPersistenceHelper.getIntegerAttributeWithDefault(gemElement, GemPersistenceConstants.RECORD_CREATION_GEM_NEXT_FIELDNAME_ATTR, nArgumentsChildElems);
} catch (BadXMLDocumentException e) {
nextPossibleFieldName = nArgumentsChildElems;
}
}
/**
* Makes a copy of the specified gem.
* @return a copy of the current RecordCreationGem
*/
RecordCreationGem makeCopy() {
if (fields.isEmpty()) {
throw new IllegalStateException("Record Creation Gem cannot be copied while fields are empty");
}
// create a new gem with a copy of the current fields list
RecordCreationGem newGem = new RecordCreationGem();
newGem.fields = this.getCopyOfFieldsList();
newGem.nextPossibleFieldName = this.nextPossibleFieldName;
newGem.updateInputs(newGem.getFieldNameToInputMap());
return newGem;
}
}