001: /*
002: * Copyright 2002-2005 the original author or authors.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package info.jtrac.domain;
018:
019: import static info.jtrac.Constants.*;
020:
021: import info.jtrac.util.XmlUtils;
022:
023: import java.io.Serializable;
024: import java.util.ArrayList;
025: import java.util.Collection;
026: import java.util.Collections;
027: import java.util.EnumMap;
028: import java.util.EnumSet;
029: import java.util.HashMap;
030: import java.util.HashSet;
031: import java.util.LinkedHashMap;
032: import java.util.LinkedList;
033: import java.util.List;
034: import java.util.Map;
035: import java.util.Set;
036: import java.util.TreeMap;
037:
038: import org.dom4j.Document;
039: import org.dom4j.Element;
040:
041: /**
042: * XML metadata is one of the interesting design decisions of JTrac.
043: * Metadata is defined for each space and so Items that belong to a
044: * space are customized by the space metadata. This class can marshall
045: * and unmarshall itself to XML and this XML is stored in the database
046: * in a single column. Because of this approach, Metadata can be made more
047: * and more complicated in the future without impact to the database schema.
048: *
049: * Things that the Metadata configures for a Space:
050: *
051: * 1) custom Fields for an Item (within a Space)
052: * - Label
053: * - whether mandatory or not [ DEPRECATED ]
054: * - the option values (drop down list options)
055: * - the option "key" values are stored in the database (WITHOUT any relationships)
056: * - the values corresponding to "key"s are resolved in memory from the Metadata
057: * and not through a database join.
058: *
059: * 2) the Roles available within a space
060: * - for each (from) State the (to) State transitions allowed for this role
061: * - and within each (from) State the fields that this Role can view / edit
062: *
063: * 3) the State labels corresponding to each state
064: * - internally States are integers, but for display we need a label
065: * - labels can be customized
066: * - special State values: 0 = New, 1 = Open, 99 = Closed
067: *
068: * 4) the order in which the fields are displayed
069: * on the data entry screens and the query result screens etc.
070: *
071: * There is one downside to this approach and that is there is a limit
072: * to the nunmbers of custom fields available. The existing limits are
073: * - Drop Down: 10
074: * - Free Text: 5
075: * - Numeric: 3
076: * - Date/Time: 3
077: *
078: * Metadata can be inherited, and this allows for "reuse" TODO
079: */
080: public class Metadata implements Serializable {
081:
082: private long id;
083: private int version;
084: private Integer type;
085: private String name;
086: private String description;
087: private Metadata parent;
088:
089: private Map<Field.Name, Field> fields;
090: private Map<String, Role> roles;
091: private Map<Integer, String> states;
092: private List<Field.Name> fieldOrder;
093:
094: public Metadata() {
095: init();
096: }
097:
098: private void init() {
099: fields = new EnumMap<Field.Name, Field>(Field.Name.class);
100: roles = new HashMap<String, Role>();
101: states = new TreeMap<Integer, String>();
102: fieldOrder = new LinkedList<Field.Name>();
103: }
104:
105: /* accessor, will be used by Hibernate */
106: public void setXmlString(String xmlString) {
107: init();
108: if (xmlString == null) {
109: return;
110: }
111: Document document = XmlUtils.parse(xmlString);
112: for (Element e : (List<Element>) document
113: .selectNodes(FIELD_XPATH)) {
114: Field field = new Field(e);
115: fields.put(field.getName(), field);
116: }
117: for (Element e : (List<Element>) document
118: .selectNodes(ROLE_XPATH)) {
119: Role role = new Role(e);
120: roles.put(role.getName(), role);
121: }
122: for (Element e : (List<Element>) document
123: .selectNodes(STATE_XPATH)) {
124: String key = e.attributeValue(STATUS);
125: String value = e.attributeValue(LABEL);
126: states.put(Integer.parseInt(key), value);
127: }
128: for (Element e : (List<Element>) document
129: .selectNodes(FIELD_ORDER_XPATH)) {
130: String fieldName = e.attributeValue(NAME);
131: fieldOrder.add(Field.convertToName(fieldName));
132: }
133: }
134:
135: /* accessor, will be used by Hibernate */
136: public String getXmlString() {
137: Document d = XmlUtils.getNewDocument(METADATA);
138: Element root = d.getRootElement();
139: Element fs = root.addElement(FIELDS);
140: for (Field field : fields.values()) {
141: field.addAsChildOf(fs);
142: }
143: Element rs = root.addElement(ROLES);
144: for (Role role : roles.values()) {
145: role.addAsChildOf(rs);
146: }
147: Element ss = root.addElement(STATES);
148: for (Map.Entry<Integer, String> entry : states.entrySet()) {
149: Element e = ss.addElement(STATE);
150: e.addAttribute(STATUS, entry.getKey() + "");
151: e.addAttribute(LABEL, entry.getValue());
152: }
153: Element fo = fs.addElement(FIELD_ORDER);
154: for (Field.Name f : fieldOrder) {
155: Element e = fo.addElement(FIELD);
156: e.addAttribute(NAME, f.toString());
157: }
158: return d.asXML();
159: }
160:
161: public String getPrettyXml() {
162: return XmlUtils.getAsPrettyXml(getXmlString());
163: }
164:
165: //====================================================================
166:
167: public void initRoles() {
168: // set up default simple workflow
169: states.put(State.NEW, "New");
170: states.put(State.OPEN, "Open");
171: states.put(State.CLOSED, "Closed");
172: addRole("DEFAULT");
173: toggleTransition("DEFAULT", State.NEW, State.OPEN);
174: toggleTransition("DEFAULT", State.OPEN, State.OPEN);
175: toggleTransition("DEFAULT", State.OPEN, State.CLOSED);
176: toggleTransition("DEFAULT", State.CLOSED, State.OPEN);
177: }
178:
179: public Field getField(String fieldName) {
180: return fields.get(Field.convertToName(fieldName));
181: }
182:
183: public void add(Field field) {
184: fields.put(field.getName(), field); // will overwrite if exists
185: if (!fieldOrder.contains(field.getName())) { // but for List, need to check
186: fieldOrder.add(field.getName());
187: }
188: for (Role role : roles.values()) {
189: for (State state : role.getStates().values()) {
190: state.add(field.getName());
191: }
192: }
193: }
194:
195: public void removeField(String fieldName) {
196: Field.Name tempName = Field.convertToName(fieldName);
197: fields.remove(tempName);
198: fieldOrder.remove(tempName);
199: for (Role role : roles.values()) {
200: for (State state : role.getStates().values()) {
201: state.remove(tempName);
202: }
203: }
204: }
205:
206: public void addState(String stateName) {
207: // first get the max of existing state keys
208: int maxStatus = 0;
209: for (int status : states.keySet()) {
210: if (status > maxStatus && status != State.CLOSED) {
211: maxStatus = status;
212: }
213: }
214: int newStatus = maxStatus + 1;
215: states.put(newStatus, stateName);
216: // by default each role will have permissions for this state, for all fields
217: for (Role role : roles.values()) {
218: State state = new State(newStatus);
219: state.add(fields.keySet());
220: role.add(state);
221: }
222: }
223:
224: public void removeState(int stateId) {
225: states.remove(stateId);
226: for (Role role : roles.values()) {
227: role.removeState(stateId);
228: }
229:
230: }
231:
232: public void addRole(String roleName) {
233: Role role = new Role(roleName);
234: for (Map.Entry<Integer, String> entry : states.entrySet()) {
235: State state = new State(entry.getKey());
236: state.add(fields.keySet());
237: role.add(state);
238: }
239: roles.put(role.getName(), role);
240: }
241:
242: public void renameRole(String oldRole, String newRole) {
243: // important! this has to be combined with a database update
244: Role role = roles.get(oldRole);
245: if (role == null) {
246: return; // TODO improve JtracTest and assert not null here
247: }
248: role.setName(newRole);
249: roles.remove(oldRole);
250: roles.put(newRole, role);
251: }
252:
253: public void removeRole(String roleName) {
254: // important! this has to be combined with a database update
255: roles.remove(roleName);
256: }
257:
258: public Set<Field.Name> getUnusedFieldNames() {
259: EnumSet<Field.Name> allFieldNames = EnumSet
260: .allOf(Field.Name.class);
261: for (Field f : getFields().values()) {
262: allFieldNames.remove(f.getName());
263: }
264: return allFieldNames;
265: }
266:
267: public Map<String, String> getAvailableFieldTypes() {
268: Map<String, String> fieldTypes = new LinkedHashMap<String, String>();
269: for (Field.Name fieldName : getUnusedFieldNames()) {
270: String fieldType = fieldTypes.get(fieldName.getType() + "");
271: if (fieldType == null) {
272: fieldTypes.put(fieldName.getType() + "", "1");
273: } else {
274: int count = Integer.parseInt(fieldType);
275: count++;
276: fieldTypes.put(fieldName.getType() + "", count + "");
277: }
278: }
279: return fieldTypes;
280: }
281:
282: public Field getNextAvailableField(int fieldType) {
283: for (Field.Name fieldName : getUnusedFieldNames()) {
284: if (fieldName.getType() == fieldType) {
285: return new Field(fieldName + "");
286: }
287: }
288: throw new RuntimeException("No field available of type "
289: + fieldType);
290: }
291:
292: // customized accessor
293: public Map<Field.Name, Field> getFields() {
294: Map<Field.Name, Field> map = fields;
295: if (parent != null) {
296: map.putAll(parent.getFields());
297: }
298: return map;
299: }
300:
301: // to make JSTL easier
302: public Collection<Role> getRoleList() {
303: return roles.values();
304: }
305:
306: public List<Field> getFieldList() {
307: Map<Field.Name, Field> map = getFields();
308: List<Field> list = new ArrayList<Field>(fields.size());
309: for (Field.Name fieldName : getFieldOrder()) {
310: list.add(fields.get(fieldName));
311: }
312: return list;
313: }
314:
315: public String getCustomValue(Field.Name fieldName, Integer key) {
316: return getCustomValue(fieldName, key + "");
317: }
318:
319: public String getCustomValue(Field.Name fieldName, String key) {
320: Field field = fields.get(fieldName);
321: if (field != null) {
322: return field.getCustomValue(key);
323: }
324: if (parent != null) {
325: return parent.getCustomValue(fieldName, key);
326: }
327: return "";
328: }
329:
330: public String getStatusValue(Integer key) {
331: if (key == null) {
332: return "";
333: }
334: String s = states.get(key);
335: if (s == null) {
336: return "";
337: }
338: return s;
339: }
340:
341: public int getRoleCount() {
342: return roles.size();
343: }
344:
345: public int getFieldCount() {
346: return getFields().size();
347: }
348:
349: public int getStateCount() {
350: return states.size();
351: }
352:
353: /**
354: * logic for resolving the next possible transitions for a given role and state
355: * - lookup Role by roleKey
356: * - for this Role, lookup state by key (integer)
357: * - for the State, iterate over transitions, get the label for each and add to map
358: * The map returned is used to render the drop down list on screen, [ key = value ]
359: */
360: public Map<Integer, String> getPermittedTransitions(
361: List<String> roleKeys, int status) {
362: Map<Integer, String> map = new LinkedHashMap<Integer, String>();
363: for (String roleKey : roleKeys) {
364: Role role = roles.get(roleKey);
365: if (role != null) {
366: State state = role.getStates().get(status);
367: if (state != null) {
368: for (int transition : state.getTransitions()) {
369: map
370: .put(transition, this .states
371: .get(transition));
372: }
373: }
374: }
375: }
376: return map;
377: }
378:
379: // returning map ideal for JSTL
380: public Map<String, Boolean> getRolesAbleToTransition(
381: int fromStatus, int toStatus) {
382: Map<String, Boolean> map = new HashMap<String, Boolean>(roles
383: .size());
384: for (Role role : roles.values()) {
385: State s = role.getStates().get(fromStatus);
386: if (s.getTransitions().contains(toStatus)) {
387: map.put(role.getName(), true);
388: }
389: }
390: return map;
391: }
392:
393: public Set<String> getRolesAbleToTransitionFrom(int state) {
394: Set<String> set = new HashSet<String>(roles.size());
395: for (Role role : roles.values()) {
396: State s = role.getStates().get(state);
397: if (s.getTransitions().size() > 0) {
398: set.add(role.getName());
399: }
400: }
401: return set;
402: }
403:
404: private State getRoleState(String roleKey, int stateKey) {
405: Role role = roles.get(roleKey);
406: return role.getStates().get(stateKey);
407: }
408:
409: public void toggleTransition(String roleKey, int fromState,
410: int toState) {
411: State state = getRoleState(roleKey, fromState);
412: if (state.getTransitions().contains(toState)) {
413: state.getTransitions().remove(toState);
414: } else {
415: state.getTransitions().add(toState);
416: }
417: }
418:
419: public void switchMask(int stateKey, String roleKey,
420: String fieldName) {
421: State state = getRoleState(roleKey, stateKey);
422: Field.Name tempName = Field.convertToName(fieldName);
423: Integer mask = state.getFields().get(tempName);
424: switch (mask) {
425: // case State.MASK_HIDDEN: state.getFields().put(name, State.MASK_READONLY); return; HIDDEN support in future
426: case State.MASK_READONLY:
427: state.getFields().put(tempName, State.MASK_OPTIONAL);
428: return;
429: case State.MASK_OPTIONAL:
430: state.getFields().put(tempName, State.MASK_MANDATORY);
431: return;
432: case State.MASK_MANDATORY:
433: state.getFields().put(tempName, State.MASK_READONLY);
434: return;
435: default: // should never happen
436: }
437: }
438:
439: public List<Field> getEditableFields(String roleKey, int status) {
440: return getEditableFields(Collections.singletonList(roleKey),
441: status);
442: }
443:
444: public List<Field> getEditableFields(Collection<String> roleKeys,
445: int status) {
446: Map<Field.Name, Field> fs = new HashMap<Field.Name, Field>(
447: getFieldCount());
448: for (String roleKey : roleKeys) {
449: if (roleKey.startsWith("ROLE_")) {
450: continue;
451: }
452: if (status > -1) {
453: State state = getRoleState(roleKey, status);
454: fs.putAll(getEditableFields(state));
455: } else { // we are trying to find all editable fields
456: Role role = roles.get(roleKey);
457: for (State state : role.getStates().values()) {
458: if (state.getStatus() == State.NEW) {
459: continue;
460: }
461: fs.putAll(getEditableFields(state));
462: }
463: }
464: }
465: // just to fix the order of the fields
466: List<Field> result = new ArrayList<Field>(getFieldCount());
467: for (Field.Name fieldName : fieldOrder) {
468: Field f = fs.get(fieldName);
469: // and not all fields may be editable
470: if (f != null) {
471: result.add(f);
472: }
473: }
474: return result;
475: }
476:
477: public List<Field> getEditableFields() {
478: return getEditableFields(roles.keySet(), -1);
479: }
480:
481: private Map<Field.Name, Field> getEditableFields(State state) {
482: Map<Field.Name, Field> fs = new HashMap<Field.Name, Field>(
483: getFieldCount());
484: for (Map.Entry<Field.Name, Integer> entry : state.getFields()
485: .entrySet()) {
486: if (entry.getValue() == State.MASK_OPTIONAL
487: || entry.getValue() == State.MASK_MANDATORY) {
488: Field f = fields.get(entry.getKey());
489: // set if optional or not, this changes depending on the user / role and status
490: f.setOptional(entry.getValue() == State.MASK_OPTIONAL);
491: fs.put(f.getName(), f);
492: }
493: }
494: return fs;
495: }
496:
497: //==================================================================
498:
499: public int getVersion() {
500: return version;
501: }
502:
503: public void setVersion(int version) {
504: this .version = version;
505: }
506:
507: public String getName() {
508: return name;
509: }
510:
511: public void setName(String name) {
512: this .name = name;
513: }
514:
515: public String getDescription() {
516: return description;
517: }
518:
519: public void setDescription(String description) {
520: this .description = description;
521: }
522:
523: public long getId() {
524: return id;
525: }
526:
527: public void setId(long id) {
528: this .id = id;
529: }
530:
531: public Integer getType() {
532: return type;
533: }
534:
535: public void setType(Integer type) {
536: this .type = type;
537: }
538:
539: public Metadata getParent() {
540: return parent;
541: }
542:
543: public void setParent(Metadata parent) {
544: this .parent = parent;
545: }
546:
547: //=======================================
548: // no setters required
549:
550: public Map<String, Role> getRoles() {
551: return roles;
552: }
553:
554: public Map<Integer, String> getStates() {
555: return states;
556: }
557:
558: public List<Field.Name> getFieldOrder() {
559: return fieldOrder;
560: }
561:
562: @Override
563: public String toString() {
564: StringBuffer sb = new StringBuffer();
565: sb.append("id [").append(id);
566: sb.append("]; parent [").append(parent);
567: sb.append("]; fields [").append(fields);
568: sb.append("]; roles [").append(roles);
569: sb.append("]; states [").append(states);
570: sb.append("]; fieldOrder [").append(fieldOrder);
571: sb.append("]");
572: return sb.toString();
573: }
574:
575: }
|