/* * file: FieldMap.java * author: Jon Iles * copyright: (c) Packwood Software 2011 * date: 13/04/2011 */ /* * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by the * Free Software Foundation; either version 2.1 of the License, or (at your * option) any later version. * * This library 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 Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; if not, write to the Free Software Foundation, Inc., * 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. */ package net.sf.mpxj.mpp; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import net.sf.mpxj.AccrueType; import net.sf.mpxj.ConstraintType; import net.sf.mpxj.CustomFieldContainer; import net.sf.mpxj.DataType; import net.sf.mpxj.Duration; import net.sf.mpxj.EarnedValueMethod; import net.sf.mpxj.FieldContainer; import net.sf.mpxj.FieldType; import net.sf.mpxj.Priority; import net.sf.mpxj.ProjectProperties; import net.sf.mpxj.Rate; import net.sf.mpxj.ResourceRequestType; import net.sf.mpxj.TaskType; import net.sf.mpxj.TimeUnit; import net.sf.mpxj.WorkGroup; import net.sf.mpxj.common.NumberHelper; /** * This class is used to represent the mapping present in the MPP file * between fields and their locations in various data blocks. */ abstract class FieldMap { /** * Constructor. * * @param properties project properties * @param customFields custom field values */ public FieldMap(ProjectProperties properties, CustomFieldContainer customFields) { m_properties = properties; m_customFields = customFields; } /** * Generic method used to create a field map from a block of data. * * @param data field map data */ private void createFieldMap(byte[] data) { int index = 0; int lastDataBlockOffset = 0; int dataBlockIndex = 0; while (index < data.length) { long mask = MPPUtility.getInt(data, index + 0); //mask = mask << 4; int dataBlockOffset = MPPUtility.getShort(data, index + 4); //int metaFlags = MPPUtility.getByte(data, index + 8); FieldType type = getFieldType(MPPUtility.getInt(data, index + 12)); int category = MPPUtility.getShort(data, index + 20); //int sizeInBytes = MPPUtility.getShort(data, index + 22); //int metaIndex = MPPUtility.getInt(data, index + 24); // // Categories // // 02 - Short values [RATE_UNITS, WORKGROUP, ACCRUE, TIME_UNITS, PRIORITY, TASK_TYPE, CONSTRAINT, ACCRUE, PERCENTAGE, SHORT, WORK_UNITS] - BOOKING_TYPE, EARNED_VALUE_METHOD, DELIVERABLE_TYPE, RESOURCE_REQUEST_TYPE - we have as string in MPXJ???? // 03 - Int values [DURATION, INTEGER] - Recalc outline codes as Boolean? // 05 - Rate, Number [RATE, NUMERIC] // 08 - String (and some durations!!!) [STRING, DURATION] // 0B - Boolean (meta block 0?) - [BOOLEAN] // 13 - Date - [DATE] // 48 - GUID - [GUID] // 64 - Boolean (meta block 1?)- [BOOLEAN] // 65 - Work, Currency [WORK, CURRENCY] // 66 - Units [UNITS] // 1D - Raw bytes [BINARY, ASCII_STRING] - Exception: outline code indexes, they are integers, but stored as part of a binary block int varDataKey; if (useTypeAsVarDataKey()) { Integer substitute = substituteVarDataKey(type); if (substitute == null) { varDataKey = (MPPUtility.getInt(data, index + 12) & 0x0000FFFF); } else { varDataKey = substitute.intValue(); } } else { varDataKey = MPPUtility.getByte(data, index + 6); } FieldLocation location; int metaBlock; switch (category) { case 0x0B: { location = FieldLocation.META_DATA; metaBlock = 0; break; } case 0x64: { location = FieldLocation.META_DATA; metaBlock = 1; break; } default: { metaBlock = 0; if (dataBlockOffset != 65535) { location = FieldLocation.FIXED_DATA; if (dataBlockOffset < lastDataBlockOffset) { ++dataBlockIndex; } lastDataBlockOffset = dataBlockOffset; int typeSize = getFixedDataFieldSize(type); if (dataBlockOffset + typeSize > m_maxFixedDataSize[dataBlockIndex]) { m_maxFixedDataSize[dataBlockIndex] = dataBlockOffset + typeSize; } } else { if (varDataKey != 0) { location = FieldLocation.VAR_DATA; } else { location = FieldLocation.UNKNOWN; } } break; } } FieldItem item = new FieldItem(type, location, dataBlockIndex, dataBlockOffset, varDataKey, mask, metaBlock); // if (location == FieldLocation.META_DATA) // { // System.out.println(MPPUtility.hexdump(data, index, 28, false) + " " + item + " mpxjDataType=" + item.getType().getDataType() + " index=" + index); // } m_map.put(type, item); index += 28; } } /** * Used to determine what value is used as the var data key. * * @return true if the field type value is used as the var data key */ protected abstract boolean useTypeAsVarDataKey(); /** * Abstract method used by child classes to supply default data. * * @return default data */ protected abstract FieldItem[] getDefaultTaskData(); /** * Abstract method used by child classes to supply default data. * * @return default data */ protected abstract FieldItem[] getDefaultResourceData(); /** * Abstract method used by child classes to supply default data. * * @return default data */ protected abstract FieldItem[] getDefaultAssignmentData(); /** * Abstract method used by child classes to supply default data. * * @return default data */ protected abstract FieldItem[] getDefaultRelationData(); /** * Given a field ID, derive the field type. * * @param fieldID field ID * @return field type */ protected abstract FieldType getFieldType(int fieldID); /** * In some circumstances the var data key used in the file * does not match the var data key derived from the type. * This method is used to perform a substitution so that * the correct value is used. * * @param type field type to be tested * @return substituted value, or null */ protected abstract Integer substituteVarDataKey(FieldType type); /** * Creates a field map for tasks. * * @param props props data */ public void createTaskFieldMap(Props props) { byte[] fieldMapData = null; for (Integer key : TASK_KEYS) { fieldMapData = props.getByteArray(key); if (fieldMapData != null) { break; } } if (fieldMapData == null) { populateDefaultData(getDefaultTaskData()); } else { createFieldMap(fieldMapData); } } /** * Creates a field map for relations. * * @param props props data */ public void createRelationFieldMap(Props props) { byte[] fieldMapData = null; for (Integer key : RELATION_KEYS) { fieldMapData = props.getByteArray(key); if (fieldMapData != null) { break; } } if (fieldMapData == null) { populateDefaultData(getDefaultRelationData()); } else { createFieldMap(fieldMapData); } } /** * Create a field map for enterprise custom fields. * * @param props props data * @param c target class */ public void createEnterpriseCustomFieldMap(Props props, Class<?> c) { byte[] fieldMapData = null; for (Integer key : ENTERPRISE_CUSTOM_KEYS) { fieldMapData = props.getByteArray(key); if (fieldMapData != null) { break; } } if (fieldMapData != null) { int index = 4; while (index < fieldMapData.length) { //Looks like the custom fields have varying types, it may be that the last byte of the four represents the type? //System.out.println(MPPUtility.hexdump(fieldMapData, index, 4, false)); int typeValue = MPPUtility.getInt(fieldMapData, index); FieldType type = getFieldType(typeValue); if (type != null && type.getClass() == c && type.toString().startsWith("Enterprise Custom Field")) { int varDataKey = (typeValue & 0xFFFF); FieldItem item = new FieldItem(type, FieldLocation.VAR_DATA, 0, 0, varDataKey, 0, 0); m_map.put(type, item); //System.out.println(item); } //System.out.println((type == null ? "?" : type.getClass().getSimpleName() + "." + type) + " " + Integer.toHexString(typeValue)); index += 4; } } } /** * Creates a field map for resources. * * @param props props data */ public void createResourceFieldMap(Props props) { byte[] fieldMapData = null; for (Integer key : RESOURCE_KEYS) { fieldMapData = props.getByteArray(key); if (fieldMapData != null) { break; } } if (fieldMapData == null) { populateDefaultData(getDefaultResourceData()); } else { createFieldMap(fieldMapData); } } /** * Creates a field map for assignments. * * @param props props data */ public void createAssignmentFieldMap(Props props) { //System.out.println("ASSIGN"); byte[] fieldMapData = null; for (Integer key : ASSIGNMENT_KEYS) { fieldMapData = props.getByteArray(key); if (fieldMapData != null) { break; } } if (fieldMapData == null) { populateDefaultData(getDefaultAssignmentData()); } else { createFieldMap(fieldMapData); } } /** * This method takes an array of data and uses this to populate the * field map. * * @param defaultData field map default data */ private void populateDefaultData(FieldItem[] defaultData) { for (FieldItem item : defaultData) { m_map.put(item.getType(), item); } } /** * Given a container, and a set of raw data blocks, this method extracts * the field data and writes it into the container. * * @param type expected type * @param container field container * @param id entity ID * @param fixedData fixed data block * @param varData var data block */ public void populateContainer(Class<? extends FieldType> type, FieldContainer container, Integer id, byte[][] fixedData, Var2Data varData) { //System.out.println(container.getClass().getSimpleName()+": " + id); for (FieldItem item : m_map.values()) { if (item.getType().getClass().equals(type)) { //System.out.println(item.m_type); Object value = item.read(id, fixedData, varData); //System.out.println(item.m_type.getClass().getSimpleName() + "." + item.m_type + ": " + value); container.set(item.getType(), value); } } } /** * Retrieve the maximum offset in the fixed data block. * * @param blockIndex required block index * @return maximum offset */ public int getMaxFixedDataSize(int blockIndex) { return m_maxFixedDataSize[blockIndex]; } /** * Retrieve the fixed data offset for a specific field. * * @param type field type * @return offset */ public int getFixedDataOffset(FieldType type) { int result; FieldItem item = m_map.get(type); if (item != null) { result = item.getFixedDataOffset(); } else { result = -1; } return result; } /** * Retrieve the var data key for a specific field. * * @param type field type * @return var data key */ public Integer getVarDataKey(FieldType type) { Integer result = null; FieldItem item = m_map.get(type); if (item != null) { result = item.getVarDataKey(); } return result; } /** * Used to map from a var data key to a field type. Note this * is designed for diagnostic use only, and uses an inefficient search. * * @param key var data key * @return field type */ public FieldType getFieldTypeFromVarDataKey(Integer key) { FieldType result = null; for (Entry<FieldType, FieldMap.FieldItem> entry : m_map.entrySet()) { if (entry.getValue().getFieldLocation() == FieldLocation.VAR_DATA && entry.getValue().getVarDataKey().equals(key)) { result = entry.getKey(); break; } } return result; } /** * Retrieve the field location for a specific field. * * @param type field type * @return field location */ public FieldLocation getFieldLocation(FieldType type) { FieldLocation result = null; FieldItem item = m_map.get(type); if (item != null) { result = item.getFieldLocation(); } return result; } /** * Retrieve a single field value. * * @param id parent entity ID * @param type field type * @param fixedData fixed data block * @param varData var data block * @return field value */ protected Object getFieldData(Integer id, FieldType type, byte[][] fixedData, Var2Data varData) { Object result = null; FieldItem item = m_map.get(type); if (item != null) { result = item.read(id, fixedData, varData); } return result; } /** * Retrieve the project properties. * * @return project file */ protected ProjectProperties getProjectProperties() { return m_properties; } /** * Clear the field map. */ public void clear() { m_map.clear(); Arrays.fill(m_maxFixedDataSize, 0); } /** * Diagnostic method used to dump known field map data. * * @param props props block containing field map data */ public void dumpKnownFieldMaps(Props props) { //for (int key=131092; key < 131098; key++) for (int key = 50331668; key < 50331674; key++) { byte[] fieldMapData = props.getByteArray(Integer.valueOf(key)); if (fieldMapData != null) { System.out.println("KEY: " + key); createFieldMap(fieldMapData); System.out.println(toString()); clear(); } } } /** * Determine the size of a field in a fixed data block. * * @param type field data type * @return field size in bytes */ private int getFixedDataFieldSize(FieldType type) { int result = 0; DataType dataType = type.getDataType(); if (dataType != null) { switch (dataType) { case DATE: case INTEGER: case DURATION: { result = 4; break; } case TIME_UNITS: case CONSTRAINT: case PRIORITY: case PERCENTAGE: case TASK_TYPE: case ACCRUE: case SHORT: case BOOLEAN: case DELAY: case WORKGROUP: case RATE_UNITS: case EARNED_VALUE_METHOD: case RESOURCE_REQUEST_TYPE: { result = 2; break; } case CURRENCY: case UNITS: case RATE: case WORK: { result = 8; break; } case WORK_UNITS: { result = 1; break; } case GUID: { result = 16; break; } default: { result = 0; break; } } } return result; } /** * {@inheritDoc} */ @Override public String toString() { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); ArrayList<FieldItem> items = new ArrayList<FieldItem>(m_map.values()); Collections.sort(items); pw.println("[FieldMap"); for (int loop = 0; loop < m_maxFixedDataSize.length; loop++) { pw.print(" MaxFixedOffset (block "); pw.print(loop); pw.print(")="); pw.println(m_maxFixedDataSize[loop]); } for (FieldItem item : items) { pw.print(" "); pw.println(item); } pw.println("]"); pw.close(); return sw.toString(); } /** * Enumeration representing the location of field data. */ enum FieldLocation { FIXED_DATA, VAR_DATA, META_DATA, UNKNOWN } /** * This class is used to collect together the attributes necessary to * describe the location of each field within the MPP file. It also provides * the methods used to extract an individual field value. */ public class FieldItem implements Comparable<FieldItem> { /** * Constructor. * * @param type field type * @param location identifies which block the field is present in * @param fixedDataBlockIndex identifies which block the data comes from * @param fixedDataOffset fixed data block offset * @param varDataKey var data block key * @param mask TODO * @param metaBlock TODO */ FieldItem(FieldType type, FieldLocation location, int fixedDataBlockIndex, int fixedDataOffset, int varDataKey, long mask, int metaBlock) { m_type = type; m_location = location; m_fixedDataBlockIndex = fixedDataBlockIndex; m_fixedDataOffset = fixedDataOffset; m_varDataKey = Integer.valueOf(varDataKey); m_mask = mask; m_metaBlock = metaBlock; } /** * Reads a single field value. * * @param id parent entity ID * @param fixedData fixed data block * @param varData var data block * @return field value */ public Object read(Integer id, byte[][] fixedData, Var2Data varData) { Object result = null; switch (m_location) { case FIXED_DATA: { result = readFixedData(id, fixedData, varData); break; } case VAR_DATA: { result = readVarData(id, fixedData, varData); break; } case META_DATA: { // We know that the Boolean flags are stored in the // "meta data" block, and can see that the first // four bytes of each row read from the field map // data in the MPP file represents a bit mask... but // we just haven't worked out how to convert this into // the actual location in the data. For now we rely on // the location in the file being fixed. This is why // we ignore the META_DATA case. break; } default: { // Unknown location - ignore this. break; } } return result; } /** * Read a field from the fixed data block. * * @param id parent entity ID * @param fixedData fixed data block * @param varData var data block * @return field value */ private Object readFixedData(Integer id, byte[][] fixedData, Var2Data varData) { Object result = null; if (m_fixedDataBlockIndex < fixedData.length) { byte[] data = fixedData[m_fixedDataBlockIndex]; if (data != null && m_fixedDataOffset < data.length) { switch (m_type.getDataType()) { case DATE: { result = MPPUtility.getTimestamp(data, m_fixedDataOffset); break; } case INTEGER: { result = Integer.valueOf(MPPUtility.getInt(data, m_fixedDataOffset)); break; } case DURATION: { FieldType unitsType = m_type.getUnitsType(); TimeUnit units = (TimeUnit) getFieldData(id, unitsType, fixedData, varData); if (units == null) { units = getProjectProperties().getDefaultDurationUnits(); } result = MPPUtility.getAdjustedDuration(getProjectProperties(), MPPUtility.getInt(data, m_fixedDataOffset), units); break; } case TIME_UNITS: { result = MPPUtility.getDurationTimeUnits(MPPUtility.getShort(data, m_fixedDataOffset), getProjectProperties().getDefaultDurationUnits()); break; } case CONSTRAINT: { result = ConstraintType.getInstance(MPPUtility.getShort(data, m_fixedDataOffset)); break; } case PRIORITY: { result = Priority.getInstance(MPPUtility.getShort(data, m_fixedDataOffset)); break; } case PERCENTAGE: { result = MPPUtility.getPercentage(data, m_fixedDataOffset); break; } case TASK_TYPE: { result = TaskType.getInstance(MPPUtility.getShort(data, m_fixedDataOffset)); break; } case ACCRUE: { result = AccrueType.getInstance(MPPUtility.getShort(data, m_fixedDataOffset)); break; } case CURRENCY: case UNITS: { result = NumberHelper.getDouble(MPPUtility.getDouble(data, m_fixedDataOffset) / 100); break; } case RATE: { result = new Rate(MPPUtility.getDouble(data, m_fixedDataOffset), TimeUnit.HOURS); break; } case WORK: { result = Duration.getInstance(MPPUtility.getDouble(data, m_fixedDataOffset) / 60000, TimeUnit.HOURS); break; } case SHORT: { result = Integer.valueOf(MPPUtility.getShort(data, m_fixedDataOffset)); break; } case BOOLEAN: { result = Boolean.valueOf(MPPUtility.getShort(data, m_fixedDataOffset) != 0); break; } case DELAY: { result = MPPUtility.getDuration(MPPUtility.getShort(data, m_fixedDataOffset), TimeUnit.HOURS); break; } case WORK_UNITS: { int variableRateUnitsValue = MPPUtility.getByte(data, m_fixedDataOffset); result = variableRateUnitsValue == 0 ? null : MPPUtility.getWorkTimeUnits(variableRateUnitsValue); break; } case WORKGROUP: { result = WorkGroup.getInstance(MPPUtility.getShort(data, m_fixedDataOffset)); break; } case RATE_UNITS: { result = TimeUnit.getInstance(MPPUtility.getShort(data, m_fixedDataOffset) - 1); break; } case EARNED_VALUE_METHOD: { result = EarnedValueMethod.getInstance(MPPUtility.getShort(data, m_fixedDataOffset)); break; } case RESOURCE_REQUEST_TYPE: { result = ResourceRequestType.getInstance(MPPUtility.getShort(data, m_fixedDataOffset)); break; } case GUID: { result = MPPUtility.getGUID(data, m_fixedDataOffset); break; } case BINARY: { // Do nothing for binary data break; } default: { //System.out.println("**** UNSUPPORTED FIXED DATA TYPE"); break; } } } } return result; } /** * Read a field value from a var data block. * * @param id parent entity ID * @param fixedData fixed data block * @param varData var data block * @return field value */ private Object readVarData(Integer id, byte[][] fixedData, Var2Data varData) { Object result = null; switch (m_type.getDataType()) { case DURATION: { FieldType unitsType = m_type.getUnitsType(); TimeUnit units = (TimeUnit) getFieldData(id, unitsType, fixedData, varData); if (units == null) { units = TimeUnit.HOURS; } result = getCustomFieldDurationValue(varData, id, m_varDataKey, units); break; } case TIME_UNITS: { result = MPPUtility.getDurationTimeUnits(varData.getShort(id, m_varDataKey), getProjectProperties().getDefaultDurationUnits()); break; } case CURRENCY: { result = NumberHelper.getDouble(varData.getDouble(id, m_varDataKey) / 100); break; } case STRING: { result = getCustomFieldUnicodeStringValue(varData, id, m_varDataKey); break; } case DATE: { result = getCustomFieldTimestampValue(varData, id, m_varDataKey); break; } case NUMERIC: { result = getCustomFieldDoubleValue(varData, id, m_varDataKey); break; } case INTEGER: { result = Integer.valueOf(varData.getInt(id, m_varDataKey)); break; } case WORK: { result = Duration.getInstance(varData.getDouble(id, m_varDataKey) / 60000, TimeUnit.HOURS); break; } case ASCII_STRING: { result = varData.getString(id, m_varDataKey); break; } case DELAY: { result = MPPUtility.getDuration(varData.getShort(id, m_varDataKey), TimeUnit.HOURS); break; } case WORK_UNITS: { int variableRateUnitsValue = varData.getByte(id, m_varDataKey); result = variableRateUnitsValue == 0 ? null : MPPUtility.getWorkTimeUnits(variableRateUnitsValue); break; } case RATE_UNITS: { result = TimeUnit.getInstance(varData.getShort(id, m_varDataKey) - 1); break; } case EARNED_VALUE_METHOD: { result = EarnedValueMethod.getInstance(varData.getShort(id, m_varDataKey)); break; } case RESOURCE_REQUEST_TYPE: { result = ResourceRequestType.getInstance(varData.getShort(id, m_varDataKey)); break; } case ACCRUE: { result = AccrueType.getInstance(varData.getShort(id, m_varDataKey)); break; } case SHORT: { result = Integer.valueOf(varData.getShort(id, m_varDataKey)); break; } case BOOLEAN: { result = Boolean.valueOf(varData.getShort(id, m_varDataKey) != 0); break; } case WORKGROUP: { result = WorkGroup.getInstance(varData.getShort(id, m_varDataKey)); break; } case GUID: { result = MPPUtility.getGUID(varData.getByteArray(id, m_varDataKey), 0); break; } case BINARY: { // Do nothing for binary data break; } default: { //System.out.println("**** UNSUPPORTED VAR DATA TYPE"); break; } } return result; } /** * Retrieve custom field value. * * @param varData var data block * @param id item ID * @param type item type * @return item value */ private Object getCustomFieldTimestampValue(Var2Data varData, Integer id, Integer type) { Object result = null; // // Note that this simplistic approach could produce false positives // int mask = varData.getShort(id, type); if ((mask & 0xFF00) != VALUE_LIST_MASK) { result = getRawTimestampValue(varData, id, type); } else { int uniqueId = varData.getInt(id, 2, type); CustomFieldValueItem item = m_customFields.getCustomFieldValueItemByUniqueID(uniqueId); if (item != null) { Object value = item.getValue(); if (value instanceof Date) { result = value; } } // // If we can't find a custom field value with this ID, fall back to treating this as a normal value // if (result == null) { result = getRawTimestampValue(varData, id, type); } } return result; } /** * Retrieve a timestamp value. * * @param varData var data block * @param id item ID * @param type item type * @return item value */ private Object getRawTimestampValue(Var2Data varData, Integer id, Integer type) { Object result = null; byte[] data = varData.getByteArray(id, type); if (data != null) { if (data.length == 512) { result = MPPUtility.getUnicodeString(data, 0); } else { if (data.length >= 4) { result = MPPUtility.getTimestamp(data, 0); } } } return result; } /** * Retrieve custom field value. * * @param varData var data block * @param id item ID * @param type item type * @param units duration units * @return item value */ private Object getCustomFieldDurationValue(Var2Data varData, Integer id, Integer type, TimeUnit units) { Object result = null; byte[] data = varData.getByteArray(id, type); if (data != null) { if (data.length == 512) { result = MPPUtility.getUnicodeString(data, 0); } else { if (data.length >= 4) { int duration = MPPUtility.getInt(data, 0); result = MPPUtility.getAdjustedDuration(getProjectProperties(), duration, units); } } } return result; } /** * Retrieve custom field value. * * @param varData var data block * @param id item ID * @param type item type * @return item value */ private Double getCustomFieldDoubleValue(Var2Data varData, Integer id, Integer type) { double result = 0; // // Note that this simplistic approach could produce false positives // int mask = varData.getShort(id, type); if ((mask & 0xFF00) != VALUE_LIST_MASK) { result = varData.getDouble(id, type); } else { int uniqueId = varData.getInt(id, 2, type); CustomFieldValueItem item = m_customFields.getCustomFieldValueItemByUniqueID(uniqueId); if (item != null) { Object value = item.getValue(); if (value instanceof Number) { result = ((Number) value).doubleValue(); } } } return NumberHelper.getDouble(result); } /** * Retrieve custom field value. * * @param varData var data block * @param id item ID * @param type item type * @return item value */ private String getCustomFieldUnicodeStringValue(Var2Data varData, Integer id, Integer type) { String result = null; // // Note that this simplistic approach could produce false positives // int mask = varData.getShort(id, type); if ((mask & 0xFF00) != VALUE_LIST_MASK) { result = varData.getUnicodeString(id, type); } else { int uniqueId = varData.getInt(id, 2, type); CustomFieldValueItem item = m_customFields.getCustomFieldValueItemByUniqueID(uniqueId); if (item != null) { Object value = item.getValue(); if (value instanceof String) { result = (String) value; } } } return result; } /** * Retrieve the field type. * * @return field type */ public FieldType getType() { return m_type; } /** * Retrieve the index of the fixed data block containing this item. * * @return fixed data block index */ public int getFixedDataBlockIndex() { return m_fixedDataBlockIndex; } /** * Retrieve the fixed data offset for this field. * * @return fixed data offset */ public int getFixedDataOffset() { return m_fixedDataOffset; } /** * Retrieve the var data key for this field. * * @return var data key */ public Integer getVarDataKey() { return m_varDataKey; } /** * Retrieve the field location for this field. * * @return field location */ public FieldLocation getFieldLocation() { return m_location; } /** * Implements the only method in the Comparable interface to allow * FieldItem instances to be sorted. * * @param item item to compare with * @return comparison result */ @Override public int compareTo(FieldItem item) { int result = m_location.compareTo(item.m_location); if (result == 0) { switch (m_location) { case FIXED_DATA: { result = m_fixedDataBlockIndex - item.m_fixedDataBlockIndex; if (result == 0) { result = m_fixedDataOffset - item.m_fixedDataOffset; } break; } case VAR_DATA: { result = m_varDataKey.intValue() - item.m_varDataKey.intValue(); break; } default: { break; } } } return result; } /** * {@inheritDoc} */ @Override public String toString() { StringBuilder buffer = new StringBuilder(); buffer.append("[FieldItem type="); buffer.append(m_type); buffer.append(" location="); buffer.append(m_location); switch (m_location) { case FIXED_DATA: { buffer.append(" fixedDataBlockIndex="); buffer.append(m_fixedDataBlockIndex); buffer.append(" fixedDataBlockOffset="); buffer.append(m_fixedDataOffset); break; } case VAR_DATA: { buffer.append(" varDataKey="); buffer.append(m_varDataKey); break; } case META_DATA: { buffer.append(" mask="); buffer.append(Long.toHexString(m_mask)); buffer.append(" block="); buffer.append(m_metaBlock); break; } default: { break; } } buffer.append("]"); return buffer.toString(); } private FieldType m_type; private FieldLocation m_location; private int m_fixedDataBlockIndex; private int m_fixedDataOffset; private Integer m_varDataKey; private long m_mask; private int m_metaBlock; } private ProjectProperties m_properties; protected CustomFieldContainer m_customFields; private Map<FieldType, FieldItem> m_map = new HashMap<FieldType, FieldItem>(); private int[] m_maxFixedDataSize = new int[MAX_FIXED_DATA_BLOCKS]; private static final Integer[] TASK_KEYS = { Props.TASK_FIELD_MAP, Props.TASK_FIELD_MAP2 }; private static final Integer[] ENTERPRISE_CUSTOM_KEYS = { Props.ENTERPRISE_CUSTOM_FIELD_MAP }; private static final Integer[] RESOURCE_KEYS = { Props.RESOURCE_FIELD_MAP, Props.RESOURCE_FIELD_MAP2 }; private static final Integer[] ASSIGNMENT_KEYS = { Props.ASSIGNMENT_FIELD_MAP, Props.ASSIGNMENT_FIELD_MAP2 }; private static final Integer[] RELATION_KEYS = { Props.RELATION_FIELD_MAP }; private static final int VALUE_LIST_MASK = 0x0700; private static final int MAX_FIXED_DATA_BLOCKS = 2; }