001: package org.drools.repository;
002:
003: import java.io.InputStream;
004: import java.util.ArrayList;
005: import java.util.Calendar;
006: import java.util.Collections;
007: import java.util.Comparator;
008: import java.util.Iterator;
009: import java.util.List;
010:
011: import javax.jcr.AccessDeniedException;
012: import javax.jcr.ItemExistsException;
013: import javax.jcr.ItemNotFoundException;
014: import javax.jcr.Node;
015: import javax.jcr.PathNotFoundException;
016: import javax.jcr.Property;
017: import javax.jcr.RepositoryException;
018: import javax.jcr.query.Query;
019: import javax.jcr.query.QueryResult;
020:
021: import org.apache.log4j.Logger;
022:
023: /**
024: * A PackageItem object aggregates a set of assets (for example, rules). This is advantageous for systems using the JBoss Rules
025: * engine where the application might make use of many related rules.
026: * <p>
027: * A PackageItem refers to rule nodes within the RulesRepository. It contains the "master copy" of assets (which may be linked
028: * into other packages or other types of containers).
029: * This is a container "node".
030: *
031: * @author btruitt
032: */
033: public class PackageItem extends VersionableItem {
034: private static Logger log = Logger.getLogger(PackageItem.class);
035:
036: /**
037: * This is the name of the rules "subfolder" where rules are kept
038: * for this package.
039: */
040: public static final String ASSET_FOLDER_NAME = "assets";
041:
042: /**
043: * The dublin core format attribute.
044: */
045: public static final String PACKAGE_FORMAT = "package";
046:
047: /**
048: * The name of the rule package node type
049: */
050: public static final String RULE_PACKAGE_TYPE_NAME = "drools:packageNodeType";
051:
052: public static final String HEADER_PROPERTY_NAME = "drools:header";
053: public static final String EXTERNAL_URI_PROPERTY_NAME = "drools:externalURI";
054:
055: private static final String COMPILED_PACKAGE_PROPERTY_NAME = "drools:compiledPackage";
056:
057: /**
058: * Constructs an object of type RulePackageItem corresponding the specified node
059: * @param rulesRepository the rulesRepository that instantiated this object
060: * @param node the node to which this object corresponds
061: * @throws RulesRepositoryException
062: */
063: public PackageItem(RulesRepository rulesRepository, Node node)
064: throws RulesRepositoryException {
065: super (rulesRepository, node);
066:
067: try {
068: //make sure this node is a rule package node
069: if (!(this .node.getPrimaryNodeType().getName().equals(
070: RULE_PACKAGE_TYPE_NAME) || isHistoricalVersion())) {
071: String message = this .node.getName()
072: + " is not a node of type "
073: + RULE_PACKAGE_TYPE_NAME
074: + ". It is a node of type: "
075: + this .node.getPrimaryNodeType().getName();
076: log.error(message);
077: throw new RulesRepositoryException(message);
078: }
079: } catch (Exception e) {
080: log.error("Caught exception: " + e);
081: throw new RulesRepositoryException(e);
082: }
083: }
084:
085: PackageItem() {
086: super (null, null);
087: }
088:
089: /**
090: * Return the name of the package.
091: */
092: public String getName() {
093: try {
094:
095: if (isSnapshot()) {
096: return this .node.getParent().getName();
097: } else {
098: return super .getName();
099: }
100: } catch (RepositoryException e) {
101: throw new RulesRepositoryException(e);
102: }
103: }
104:
105: /**
106: * @return true if this package is actually a snapshot.
107: */
108: public boolean isSnapshot() {
109: try {
110: return (!this .rulesRepository.isNotSnapshot(this .node
111: .getParent()));
112: } catch (RepositoryException e) {
113: throw new IllegalStateException(e);
114: }
115: }
116:
117: /**
118: * returns the name of the snapshot, if this package is really a snapshot.
119: * If it is not, it will just return the name of the package, so use wisely !
120: */
121: public String getSnapshotName() {
122: return super .getName();
123: }
124:
125: /**
126: * Adds a rule to the current package with no category (not recommended !).
127: * Without categories, its going to be hard to find rules later on
128: * (unless packages are enough for you).
129: */
130: public AssetItem addAsset(String assetName, String description) {
131: return addAsset(assetName, description, null, null);
132: }
133:
134: /**
135: * This adds a rule to the current physical package (you can move it later).
136: * With the given category.
137: *
138: * This will NOT check the asset in, just create the basic record.
139: * @param assetName The name of the asset (the file name minus the extension)
140: * @param description A description of the asset.
141: * @param initialCategory The initial category the asset is placed in (can belong to multiple ones later).
142: * @param format The dublin core format (which also determines what editor is used) - this is effectively the file extension.
143: */
144: public AssetItem addAsset(String assetName, String description,
145: String initialCategory, String format) {
146: Node ruleNode;
147: try {
148:
149: Node rulesFolder = this .node.getNode(ASSET_FOLDER_NAME);
150: ruleNode = rulesFolder.addNode(assetName,
151: AssetItem.RULE_NODE_TYPE_NAME);
152: ruleNode.setProperty(AssetItem.TITLE_PROPERTY_NAME,
153: assetName);
154:
155: ruleNode.setProperty(AssetItem.DESCRIPTION_PROPERTY_NAME,
156: description);
157: if (format != null) {
158: ruleNode.setProperty(AssetItem.FORMAT_PROPERTY_NAME,
159: format);
160: } else {
161: ruleNode.setProperty(AssetItem.FORMAT_PROPERTY_NAME,
162: AssetItem.DEFAULT_CONTENT_FORMAT);
163: }
164:
165: ruleNode.setProperty(VersionableItem.CHECKIN_COMMENT,
166: "Initial");
167:
168: Calendar lastModified = Calendar.getInstance();
169:
170: ruleNode.setProperty(AssetItem.LAST_MODIFIED_PROPERTY_NAME,
171: lastModified);
172: ruleNode.setProperty(AssetItem.PACKAGE_NAME_PROPERTY, this
173: .getName());
174: ruleNode.setProperty(CREATOR_PROPERTY_NAME, this .node
175: .getSession().getUserID());
176:
177: AssetItem rule = new AssetItem(this .rulesRepository,
178: ruleNode);
179:
180: rule.updateState(StateItem.DRAFT_STATE_NAME);
181:
182: if (initialCategory != null) {
183: rule.addCategory(initialCategory);
184: }
185:
186: return rule;
187:
188: } catch (RepositoryException e) {
189: if (e instanceof ItemExistsException) {
190: throw new RulesRepositoryException(
191: "A rule of that name already exists in that package.",
192: e);
193: } else {
194: throw new RulesRepositoryException(e);
195: }
196: }
197:
198: }
199:
200: /**
201: * Remove an asset by name
202: * After doing this, you will need to check in the package
203: * as removing an item effects the parent package.
204: */
205: public void removeAsset(String name) {
206: try {
207: this .node.getNode(ASSET_FOLDER_NAME + "/" + name).remove();
208: } catch (RepositoryException e) {
209: throw new RulesRepositoryException(e);
210: }
211: }
212:
213: // The following should be kept for reference on how to add a reference that
214: //is either locked to a version or follows head - FOR SHARING ASSETS
215: // /**
216: // * Adds a rule to the rule package node this object represents. The reference to the rule
217: // * will optionally follow the head version of the specified rule's node or the specific
218: // * current version.
219: // *
220: // * @param ruleItem the ruleItem corresponding to the node to add to the rule package this
221: // * object represents
222: // * @param followRuleHead if true, the reference to the rule node will follow the head version
223: // * of the node, even if new versions are added. If false, will refer
224: // * specifically to the current version.
225: // * @throws RulesRepositoryException
226: // */
227: // public void addRuleReference(RuleItem ruleItem, boolean followRuleHead) throws RulesRepositoryException {
228: // try {
229: // ValueFactory factory = this.node.getSession().getValueFactory();
230: // int i = 0;
231: // Value[] newValueArray = null;
232: //
233: // try {
234: // Value[] oldValueArray = this.node.getProperty(RULE_REFERENCE_PROPERTY_NAME).getValues();
235: // newValueArray = new Value[oldValueArray.length + 1];
236: //
237: // for(i=0; i<oldValueArray.length; i++) {
238: // newValueArray[i] = oldValueArray[i];
239: // }
240: // }
241: // catch(PathNotFoundException e) {
242: // //the property has not been created yet. do so now
243: // newValueArray = new Value[1];
244: // }
245: // finally {
246: // if(newValueArray != null) { //just here to make the compiler happy
247: // if(followRuleHead) {
248: // newValueArray[i] = factory.createValue(ruleItem.getNode());
249: // }
250: // else {
251: // //this is the magic that ties it to a specific version
252: // newValueArray[i] = factory.createValue(ruleItem.getNode().getBaseVersion());
253: // }
254: // this.node.checkout();
255: // this.node.setProperty(RULE_REFERENCE_PROPERTY_NAME, newValueArray);
256: // this.node.getSession().save();
257: // this.node.checkin();
258: // }
259: // else {
260: // throw new RulesRepositoryException("Unexpected null pointer for newValueArray");
261: // }
262: // }
263: // }
264: // catch(UnsupportedRepositoryOperationException e) {
265: // String message = "";
266: // try {
267: // message = "Error: Caught UnsupportedRepositoryOperationException when attempting to get base version for rule: " + ruleItem.getNode().getName() + ". Are you sure your JCR repository supports versioning? ";
268: // log.error(message + e);
269: // }
270: // catch (RepositoryException e1) {
271: // log.error("Caught exception: " + e1);
272: // throw new RulesRepositoryException(message, e1);
273: // }
274: // log.error("Caught exception: " + e);
275: // throw new RulesRepositoryException(e);
276: // }
277: // catch(Exception e) {
278: // log.error("Caught exception: " + e);
279: // throw new RulesRepositoryException(e);
280: // }
281: // }
282:
283: //MN: The following should be kept as a reference on how to remove a version tracking reference
284: //as a compliment to the above method (which is also commented out !).
285: // /**
286: // * Removes the specified rule from the rule package node this object represents.
287: // *
288: // * @param ruleItem the ruleItem corresponding to the node to remove from the rule package
289: // * this object represents
290: // * @throws RulesRepositoryException
291: // */
292: // public void removeRuleReference(AssetItem ruleItem) throws RulesRepositoryException {
293: // try {
294: // Value[] oldValueArray = this.node.getProperty( RULE_REFERENCE_PROPERTY_NAME ).getValues();
295: // Value[] newValueArray = new Value[oldValueArray.length - 1];
296: //
297: // boolean wasThere = false;
298: //
299: // int j = 0;
300: // for ( int i = 0; i < oldValueArray.length; i++ ) {
301: // Node ruleNode = this.node.getSession().getNodeByUUID( oldValueArray[i].getString() );
302: // AssetItem currentRuleItem = new AssetItem( this.rulesRepository,
303: // ruleNode );
304: // if ( currentRuleItem.equals( ruleItem ) ) {
305: // wasThere = true;
306: // } else {
307: // newValueArray[j] = oldValueArray[i];
308: // j++;
309: // }
310: // }
311: //
312: // if ( !wasThere ) {
313: // return;
314: // } else {
315: // this.node.checkout();
316: // this.node.setProperty( RULE_REFERENCE_PROPERTY_NAME,
317: // newValueArray );
318: // this.node.getSession().save();
319: // this.node.checkin();
320: // }
321: // } catch ( PathNotFoundException e ) {
322: // //the property has not been created yet.
323: // return;
324: // } catch ( Exception e ) {
325: // log.error( "Caught exception",
326: // e );
327: // throw new RulesRepositoryException( e );
328: // }
329: // }
330:
331: //MN: This should be kept as a reference for
332: // /**
333: // * Gets a list of RuleItem objects for each rule node in this rule package
334: // *
335: // * @return the List object holding the RuleItem objects in this rule package
336: // * @throws RulesRepositoryException
337: // */
338: // public List getRules() throws RulesRepositoryException {
339: // try {
340: // Value[] valueArray = this.node.getProperty(RULE_REFERENCE_PROPERTY_NAME).getValues();
341: // List returnList = new ArrayList();
342: //
343: // for(int i=0; i<valueArray.length; i++) {
344: // Node ruleNode = this.node.getSession().getNodeByUUID(valueArray[i].getString());
345: // returnList.add(new RuleItem(this.rulesRepository, ruleNode));
346: // }
347: // return returnList;
348: // }
349: // catch(PathNotFoundException e) {
350: // //the property has not been created yet.
351: // return new ArrayList();
352: // }
353: // catch(Exception e) {
354: // log.error("Caught exception: " + e);
355: // throw new RulesRepositoryException(e);
356: // }
357: // }
358:
359: /** Return an iterator for the rules in this package */
360: public Iterator getAssets() {
361: try {
362: Node content = getVersionContentNode();
363: AssetItemIterator it = new AssetItemIterator(content
364: .getNode(ASSET_FOLDER_NAME).getNodes(),
365: this .rulesRepository);
366: return it;
367: } catch (RepositoryException e) {
368: throw new RulesRepositoryException(e);
369: }
370:
371: }
372:
373: /**
374: * This will query any assets stored under this package.
375: * For example, you can pass in <code>"drools:format = 'drl'"</code> to get a list of
376: * only a certain type of asset.
377: *
378: * @param fieldPredicates A predicate string (SQL style).
379: * @return A list of matches.
380: */
381: public AssetItemIterator queryAssets(String fieldPredicates,
382: boolean seekArchived) {
383: try {
384:
385: String sql = "SELECT * FROM "
386: + AssetItem.RULE_NODE_TYPE_NAME;
387: sql += " WHERE jcr:path LIKE '"
388: + getVersionContentNode().getPath() + "/"
389: + ASSET_FOLDER_NAME + "[%]/%'";
390: if (fieldPredicates.length() > 0) {
391: sql += " and " + fieldPredicates;
392: }
393:
394: if (seekArchived == false) {
395: sql += " AND "
396: + AssetItem.CONTENT_PROPERTY_ARCHIVE_FLAG
397: + " = 'false'";
398: }
399:
400: Query q = node.getSession().getWorkspace()
401: .getQueryManager().createQuery(sql, Query.SQL);
402: QueryResult res = q.execute();
403: return new AssetItemIterator(res.getNodes(),
404: this .rulesRepository);
405: } catch (RepositoryException e) {
406: throw new RulesRepositoryException(e);
407: }
408: }
409:
410: public AssetItemIterator queryAssets(String fieldPredicates) {
411: return queryAssets(fieldPredicates, false);
412: }
413:
414: public AssetItemIterator listArchivedAssets() {
415: return queryAssets(AssetItem.CONTENT_PROPERTY_ARCHIVE_FLAG
416: + " = 'true'", true);
417: }
418:
419: /**
420: * This will load an iterator for assets of the given format type.
421: */
422: public AssetItemIterator listAssetsByFormat(String[] formats) {
423: if (formats.length == 1) {
424: return queryAssets("drools:format='" + formats[0] + "'");
425: } else {
426: String predicate = " ( ";
427: for (int i = 0; i < formats.length; i++) {
428: predicate = predicate + "drools:format='" + formats[i]
429: + "'";
430: if (!(i == formats.length - 1)) {
431: predicate = predicate + " OR ";
432: }
433: }
434: predicate = predicate + " ) ";
435: return queryAssets(predicate);
436: }
437: }
438:
439: /**
440: * Load a specific rule asset by name.
441: */
442: public AssetItem loadAsset(String name) {
443:
444: try {
445: Node content = getVersionContentNode();
446: return new AssetItem(this .rulesRepository, content.getNode(
447: ASSET_FOLDER_NAME).getNode(name));
448: } catch (RepositoryException e) {
449: throw new RulesRepositoryException(e);
450: }
451: }
452:
453: /**
454: * Returns true if this package item contains an asset of the given name.
455: */
456: public boolean containsAsset(String name) {
457: Node content;
458: try {
459: content = getVersionContentNode();
460: return content.getNode(ASSET_FOLDER_NAME).hasNode(name);
461: } catch (RepositoryException e) {
462: throw new RulesRepositoryException(e);
463: }
464: }
465:
466: /**
467: * Nicely formats the information contained by the node that this object encapsulates
468: */
469: public String toString() {
470: try {
471: StringBuffer returnString = new StringBuffer();
472: returnString.append("Content of the rule package named "
473: + this .node.getName() + ":");
474: returnString.append("Description: " + this .getDescription()
475: + "\n");
476: returnString.append("Format: " + this .getFormat() + "\n");
477: returnString.append("Last modified: "
478: + this .getLastModified() + "\n");
479: returnString.append("Title: " + this .getTitle() + "\n");
480: returnString.append("----\n");
481:
482: return returnString.toString();
483: } catch (Exception e) {
484: log.error("Caught Exception", e);
485: return null;
486: }
487: }
488:
489: public VersionableItem getPrecedingVersion()
490: throws RulesRepositoryException {
491: try {
492: Node precedingVersionNode = this .getPrecedingVersionNode();
493: if (precedingVersionNode != null) {
494: return new PackageItem(this .rulesRepository,
495: precedingVersionNode);
496: } else {
497: return null;
498: }
499: } catch (Exception e) {
500: log.error("Caught exception", e);
501: throw new RulesRepositoryException(e);
502: }
503: }
504:
505: public VersionableItem getSucceedingVersion()
506: throws RulesRepositoryException {
507: try {
508: Node succeedingVersionNode = this
509: .getSucceedingVersionNode();
510: if (succeedingVersionNode != null) {
511: return new PackageItem(this .rulesRepository,
512: succeedingVersionNode);
513: } else {
514: return null;
515: }
516: } catch (Exception e) {
517: log.error("Caught exception", e);
518: throw new RulesRepositoryException(e);
519: }
520: }
521:
522: /**
523: * This will return a list of assets for a given state.
524: * It works through the assets that belong to this package, and
525: * if they are not in the correct state, walks backwards until it finds one
526: * in the correct state.
527: *
528: * If it walks all the way back up the versions looking for the "latest"
529: * version with the appropriate state, and can't find one,
530: * that asset is not included in the result.
531: *
532: * This will exclude any items that have the "ignoreState" set
533: * (so for example, retired items, invalid items etc).
534: *
535: * @param state The state of assets to retrieve.
536: * @param ignoreState The statuses to not include in the results (it will look
537: * at the status of the latest one).
538: */
539: public Iterator getAssetsWithStatus(final StateItem state,
540: final StateItem ignoreState) {
541: final Iterator rules = getAssets();
542:
543: List result = new ArrayList();
544: while (rules.hasNext()) {
545: AssetItem head = (AssetItem) rules.next();
546: if (head.sameState(state)) {
547: result.add(head);
548: } else if (head.sameState(ignoreState)) {
549: //ignore this one
550: } else {
551: List fullHistory = new ArrayList();
552: for (Iterator iter = head.getHistory(); iter.hasNext();) {
553: AssetItem element = (AssetItem) iter.next();
554: if (!(element.getVersionNumber() == 0)) {
555: fullHistory.add(element);
556: }
557: }
558:
559: sortHistoryByVersionNumber(fullHistory);
560:
561: Iterator prev = fullHistory.iterator();
562: while (prev.hasNext()) {
563: AssetItem prevRule = (AssetItem) prev.next();
564: if (prevRule.sameState(state)) {
565: result.add(prevRule);
566: break;
567: }
568: }
569: }
570: }
571: return result.iterator();
572: }
573:
574: void sortHistoryByVersionNumber(List fullHistory) {
575: Collections.sort(fullHistory, new Comparator() {
576:
577: public int compare(Object o1, Object o2) {
578: AssetItem a1 = (AssetItem) o1;
579: AssetItem a2 = (AssetItem) o2;
580: long la1 = a1.getVersionNumber();
581: long la2 = a2.getVersionNumber();
582: if (la1 == la2)
583: return 0;
584: else if (la1 < la2)
585: return 1;
586: else
587: return -1;
588:
589: }
590:
591: });
592: }
593:
594: /**
595: * This will return a list of assets for a given state.
596: * It works through the assets that belong to this package, and
597: * if they are not in the correct state, walks backwards until it finds one
598: * in the correct state.
599: *
600: * If it walks all the way back up the versions looking for the "latest"
601: * version with the appropriate state, and can't find one,
602: * that asset is not included in the result.
603: */
604: public Iterator getAssetsWithStatus(final StateItem state) {
605: return getAssetsWithStatus(state, null);
606: }
607:
608: /**
609: * @return The header contents as pertains to a package of rule assets.
610: */
611: public String getHeader() {
612: return this .getStringProperty(HEADER_PROPERTY_NAME);
613: }
614:
615: /**
616: * @return The external URI which will be used to sync this package to an external resource.
617: * Generally this will resolve to a directory in (for example) Subversion - with each asset
618: * being a file (with the format property as the file extension).
619: */
620: public String getExternalURI() {
621: return this .getStringProperty(EXTERNAL_URI_PROPERTY_NAME);
622: }
623:
624: public void updateHeader(String header) {
625: updateStringProperty(header, HEADER_PROPERTY_NAME);
626: }
627:
628: public void updateExternalURI(String uri) {
629: updateStringProperty(uri, EXTERNAL_URI_PROPERTY_NAME);
630: }
631:
632: /**
633: * Update the checkin comment.
634: */
635: public void updateCheckinComment(String comment) {
636: updateStringProperty(comment, VersionableItem.CHECKIN_COMMENT);
637: }
638:
639: /**
640: * This will change the status of this package, and all the contained assets.
641: * No new versions are created of anything.
642: * @param newState The status tag to change it to.
643: */
644: public void changeStatus(String newState) {
645: StateItem stateItem = rulesRepository.getState(newState);
646: updateState(stateItem);
647: for (Iterator iter = getAssets(); iter.hasNext();) {
648: AssetItem element = (AssetItem) iter.next();
649: element.updateState(stateItem);
650: }
651: }
652:
653: /**
654: * If the asset is a binary asset, then use this to update the content
655: * (do NOT use text).
656: */
657: public PackageItem updateCompiledPackage(InputStream data) {
658: checkout();
659: try {
660: this .node.setProperty(COMPILED_PACKAGE_PROPERTY_NAME, data);
661: this .node.setProperty(LAST_MODIFIED_PROPERTY_NAME, Calendar
662: .getInstance());
663: return this ;
664: } catch (RepositoryException e) {
665: log.error("Unable to update the assets binary content", e);
666: throw new RulesRepositoryException(e);
667: }
668: }
669:
670: /**
671: * This is a convenience method for returning the binary data as a byte array.
672: */
673: public byte[] getCompiledPackageBytes() {
674:
675: try {
676: Node ruleNode = getVersionContentNode();
677: if (ruleNode.hasProperty(COMPILED_PACKAGE_PROPERTY_NAME)) {
678: Property data = ruleNode
679: .getProperty(COMPILED_PACKAGE_PROPERTY_NAME);
680: InputStream in = data.getStream();
681:
682: // Create the byte array to hold the data
683: byte[] bytes = new byte[(int) data.getLength()];
684:
685: // Read in the bytes
686: int offset = 0;
687: int numRead = 0;
688: while (offset < bytes.length
689: && (numRead = in.read(bytes, offset,
690: bytes.length - offset)) >= 0) {
691: offset += numRead;
692: }
693:
694: // Ensure all the bytes have been read in
695: if (offset < bytes.length) {
696: throw new RulesRepositoryException(
697: "Could not completely read binary package for "
698: + getName());
699: }
700:
701: // Close the input stream and return bytes
702: in.close();
703: return bytes;
704: } else {
705: return null;
706: }
707: } catch (Exception e) {
708: log.error(e);
709: if (e instanceof RuntimeException)
710: throw (RuntimeException) e;
711: throw new RulesRepositoryException(e);
712: }
713: }
714:
715: }
|