View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2014 wcm.io
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  package io.wcm.tooling.commons.packmgr.unpack;
21  
22  import static org.apache.jackrabbit.vault.util.Constants.DOT_CONTENT_XML;
23  import static org.apache.jackrabbit.vault.util.Constants.ROOT_DIR;
24  
25  import java.io.File;
26  import java.io.FileOutputStream;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.OutputStream;
30  import java.math.BigDecimal;
31  import java.util.ArrayList;
32  import java.util.Calendar;
33  import java.util.Enumeration;
34  import java.util.HashSet;
35  import java.util.LinkedHashSet;
36  import java.util.List;
37  import java.util.Set;
38  import java.util.TreeSet;
39  import java.util.concurrent.atomic.AtomicBoolean;
40  import java.util.regex.Matcher;
41  import java.util.regex.Pattern;
42  import java.util.regex.PatternSyntaxException;
43  
44  import javax.jcr.Binary;
45  import javax.jcr.Item;
46  import javax.jcr.ItemVisitor;
47  import javax.jcr.Node;
48  import javax.jcr.Property;
49  import javax.jcr.PropertyType;
50  import javax.jcr.RepositoryException;
51  import javax.jcr.Session;
52  import javax.jcr.Value;
53  import javax.jcr.ValueFormatException;
54  import javax.jcr.nodetype.NodeType;
55  import javax.jcr.nodetype.PropertyDefinition;
56  import javax.xml.XMLConstants;
57  import javax.xml.parsers.ParserConfigurationException;
58  import javax.xml.parsers.SAXParser;
59  import javax.xml.parsers.SAXParserFactory;
60  
61  import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
62  import org.apache.commons.compress.archivers.zip.ZipFile;
63  import org.apache.commons.io.FileUtils;
64  import org.apache.commons.io.FilenameUtils;
65  import org.apache.commons.io.IOUtils;
66  import org.apache.commons.lang3.StringUtils;
67  import org.apache.jackrabbit.JcrConstants;
68  import org.apache.jackrabbit.util.ISO8601;
69  import org.apache.jackrabbit.vault.util.DocViewProperty;
70  import org.apache.jackrabbit.vault.util.PlatformNameFormat;
71  import org.jdom2.Attribute;
72  import org.jdom2.Document;
73  import org.jdom2.Element;
74  import org.jdom2.JDOMException;
75  import org.jdom2.Namespace;
76  import org.jdom2.input.SAXBuilder;
77  import org.jdom2.output.Format;
78  import org.jdom2.output.LineSeparator;
79  import org.jdom2.output.XMLOutputter;
80  import org.xml.sax.Attributes;
81  import org.xml.sax.SAXException;
82  import org.xml.sax.helpers.DefaultHandler;
83  
84  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
85  import io.wcm.tooling.commons.packmgr.PackageManagerException;
86  
87  /**
88   * Manages unpacking ZIP file content applying exclude patterns.
89   */
90  public final class ContentUnpacker {
91  
92    private static final String MIXINS_PROPERTY = "jcr:mixinTypes";
93    private static final String PRIMARYTYPE_PROPERTY = "jcr:primaryType";
94    private static final Namespace JCR_NAMESPACE = Namespace.getNamespace("jcr", "http://www.jcp.org/jcr/1.0");
95    private static final Namespace CQ_NAMESPACE = Namespace.getNamespace("cq", "http://www.day.com/jcr/cq/1.0");
96    private static final Pattern FILENAME_NAMESPACE_PATTERN = Pattern.compile("^([^:]+):(.+)$");
97  
98    private static final SAXParserFactory SAX_PARSER_FACTORY;
99    static {
100     SAX_PARSER_FACTORY = SAXParserFactory.newInstance();
101     SAX_PARSER_FACTORY.setNamespaceAware(true);
102   }
103 
104   private final Pattern[] excludeFiles;
105   private final Pattern[] excludeNodes;
106   private final Pattern[] excludeProperties;
107   private final Pattern[] excludeMixins;
108   private final boolean markReplicationActivated;
109   private final Pattern[] markReplicationActivatedIncludeNodes;
110   private final String dateLastReplicated;
111 
112   /**
113    * @param properties Configuration properties
114    */
115   public ContentUnpacker(ContentUnpackerProperties properties) {
116     this.excludeFiles = toPatternArray(properties.getExcludeFiles());
117     this.excludeNodes = toPatternArray(properties.getExcludeNodes());
118     this.excludeProperties = toPatternArray(properties.getExcludeProperties());
119     this.excludeMixins = toPatternArray(properties.getExcludeMixins());
120     this.markReplicationActivated = properties.isMarkReplicationActivated();
121     this.markReplicationActivatedIncludeNodes = toPatternArray(properties.getMarkReplicationActivatedIncludeNodes());
122 
123     if (StringUtils.isNotBlank(properties.getDateLastReplicated())) {
124       this.dateLastReplicated = properties.getDateLastReplicated();
125     }
126     else {
127       // set to current date
128       Calendar cal = Calendar.getInstance();
129       cal.set(Calendar.HOUR_OF_DAY, 0);
130       cal.set(Calendar.MINUTE, 0);
131       cal.set(Calendar.SECOND, 0);
132       cal.set(Calendar.MILLISECOND, 0);
133       this.dateLastReplicated = ISO8601.format(cal);
134     }
135   }
136 
137   private static Pattern[] toPatternArray(String[] patternStrings) {
138     if (patternStrings == null) {
139       return new Pattern[0];
140     }
141     Pattern[] patterns = new Pattern[patternStrings.length];
142     for (int i = 0; i < patternStrings.length; i++) {
143       try {
144         patterns[i] = Pattern.compile(patternStrings[i]);
145       }
146       catch (PatternSyntaxException ex) {
147         throw new PackageManagerException("Invalid regexp pattern: " + patternStrings[i], ex);
148       }
149     }
150     return patterns;
151   }
152 
153   private static boolean matches(String name, Pattern[] patterns, boolean defaultIfNotPatternsDefined) {
154     if (patterns.length == 0) {
155       return defaultIfNotPatternsDefined;
156     }
157     for (Pattern pattern : patterns) {
158       if (pattern.matcher(name).matches()) {
159         return true;
160       }
161     }
162     return false;
163   }
164 
165   private boolean applyXmlExcludes(String name) {
166     if (this.excludeNodes.length == 0 && this.excludeProperties.length == 0) {
167       return false;
168     }
169     return StringUtils.endsWith(name, ".xml");
170   }
171 
172   /**
173    * Unpacks file
174    * @param file File
175    * @param outputDirectory Output directory
176    */
177   public void unpack(File file, File outputDirectory) {
178     try (ZipFile zipFile = new ZipFile(file)) {
179       Enumeration<ZipArchiveEntry> entries = zipFile.getEntries();
180       while (entries.hasMoreElements()) {
181         ZipArchiveEntry entry = entries.nextElement();
182         if (!matches(entry.getName(), excludeFiles, false)) {
183           unpackEntry(zipFile, entry, outputDirectory);
184         }
185       }
186     }
187     catch (IOException ex) {
188       throw new PackageManagerException("Error reading content package " + file.getAbsolutePath(), ex);
189     }
190   }
191 
192   @SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_BAD_PRACTICE")
193   private void unpackEntry(ZipFile zipFile, ZipArchiveEntry entry, File outputDirectory) throws IOException {
194     if (entry.isDirectory()) {
195       File directory = FileUtils.getFile(outputDirectory, entry.getName());
196       directory.mkdirs();
197     }
198     else {
199       Set<String> namespacePrefixes = null;
200       if (applyXmlExcludes(entry.getName())) {
201         namespacePrefixes = getNamespacePrefixes(zipFile, entry);
202       }
203 
204       try (InputStream entryStream = zipFile.getInputStream(entry)) {
205         File outputFile = FileUtils.getFile(outputDirectory, entry.getName());
206         if (outputFile.exists()) {
207           outputFile.delete();
208         }
209         File directory = outputFile.getParentFile();
210         directory.mkdirs();
211 
212         try (FileOutputStream fos = new FileOutputStream(outputFile)) {
213           if (applyXmlExcludes(entry.getName()) && namespacePrefixes != null) {
214             // write file with XML filtering
215             try {
216               writeXmlWithExcludes(entry, entryStream, fos, namespacePrefixes);
217             }
218             catch (JDOMException ex) {
219               throw new PackageManagerException("Unable to parse XML file: " + entry.getName(), ex);
220             }
221           }
222           else {
223             // write file directly without XML filtering
224             IOUtils.copy(entryStream, fos);
225           }
226         }
227       }
228     }
229   }
230 
231   /**
232    * Parses XML file with namespace-aware SAX parser to get defined namespaces prefixes in order of appearance
233    * (to keep the same order when outputting the XML file again).
234    * @param zipFile ZIP file
235    * @param entry ZIP entry
236    * @return Ordered set with namespace prefixes in correct order.
237    *         Returns null if given XML file does not contain FileVault XML content.
238    */
239   private Set<String> getNamespacePrefixes(ZipFile zipFile, ZipArchiveEntry entry) throws IOException {
240     try (InputStream entryStream = zipFile.getInputStream(entry)) {
241       SAXParser parser = SAX_PARSER_FACTORY.newSAXParser();
242       final Set<String> prefixes = new LinkedHashSet<>();
243 
244       final AtomicBoolean foundRootElement = new AtomicBoolean(false);
245       DefaultHandler handler = new DefaultHandler() {
246         @Override
247         public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
248           // validate that XML file contains FileVault XML content
249           if (StringUtils.equals(uri, JCR_NAMESPACE.getURI()) && StringUtils.equals(localName, "root")) {
250             foundRootElement.set(true);
251           }
252         }
253         @Override
254         public void startPrefixMapping(String prefix, String uri) throws SAXException {
255           if (StringUtils.isNotBlank(prefix)) {
256             prefixes.add(prefix);
257           }
258         }
259       };
260       parser.parse(entryStream, handler);
261 
262       if (!foundRootElement.get()) {
263         return null;
264       }
265       else {
266         return prefixes;
267       }
268     }
269     catch (IOException | SAXException | ParserConfigurationException ex) {
270       throw new IOException("Error parsing " + entry.getName(), ex);
271     }
272   }
273 
274   private void writeXmlWithExcludes(ZipArchiveEntry entry, InputStream inputStream, OutputStream outputStream, Set<String> namespacePrefixes)
275       throws IOException, JDOMException {
276     SAXBuilder saxBuilder = new SAXBuilder();
277     saxBuilder.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
278     saxBuilder.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
279     Document doc = saxBuilder.build(inputStream);
280 
281     Set<String> namespacePrefixesActuallyUsed = new HashSet<>();
282 
283     // check for namespace prefix in file name
284     String namespacePrefix = getNamespacePrefix(entry.getName());
285     if (namespacePrefix != null) {
286       namespacePrefixesActuallyUsed.add(namespacePrefix);
287     }
288 
289     applyXmlExcludes(doc.getRootElement(), getParentPath(entry), namespacePrefixesActuallyUsed, false);
290 
291     XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat()
292         .setIndent("    ")
293         .setLineSeparator(LineSeparator.UNIX));
294     outputter.setXMLOutputProcessor(new OneAttributePerLineXmlProcessor(namespacePrefixes, namespacePrefixesActuallyUsed));
295     outputter.output(doc, outputStream);
296     outputStream.flush();
297   }
298 
299   static String getNamespacePrefix(String path) {
300     String fileName = FilenameUtils.getName(path);
301     if (StringUtils.equals(DOT_CONTENT_XML, fileName)) {
302       String parentFolderName = FilenameUtils.getName(FilenameUtils.getPathNoEndSeparator(path));
303       if (parentFolderName != null) {
304         String nodeName = PlatformNameFormat.getRepositoryName(parentFolderName);
305         Matcher matcher = FILENAME_NAMESPACE_PATTERN.matcher(nodeName);
306         if (matcher.matches()) {
307           return matcher.group(1);
308         }
309       }
310     }
311     return null;
312   }
313 
314   private String getParentPath(ZipArchiveEntry entry) {
315     return StringUtils.removeEnd(StringUtils.removeStart(entry.getName(), ROOT_DIR), "/" + DOT_CONTENT_XML);
316   }
317 
318   private String buildElementPath(Element element, String parentPath) {
319     StringBuilder path = new StringBuilder(parentPath);
320     if (!StringUtils.equals(element.getQualifiedName(), "jcr:root")) {
321       path.append("/").append(element.getQualifiedName());
322     }
323     return path.toString();
324   }
325 
326   private void applyXmlExcludes(Element element, String parentPath, Set<String> namespacePrefixesActuallyUsed,
327       boolean insideReplicationElement) {
328     String path = buildElementPath(element, parentPath);
329     if (matches(path, this.excludeNodes, false)) {
330       element.detach();
331       return;
332     }
333     collectNamespacePrefix(namespacePrefixesActuallyUsed, element.getNamespacePrefix());
334 
335     String jcrPrimaryType = element.getAttributeValue("primaryType", JCR_NAMESPACE);
336     boolean isRepositoryUserGroup = StringUtils.equals(jcrPrimaryType, "rep:User") || StringUtils.equals(jcrPrimaryType, "rep:Group");
337     boolean isReplicationElement = StringUtils.equals(jcrPrimaryType, "cq:Page")
338         || StringUtils.equals(jcrPrimaryType, "dam:Asset")
339         || StringUtils.equals(jcrPrimaryType, "cq:Template");
340     boolean isContent = insideReplicationElement && StringUtils.equals(element.getQualifiedName(), "jcr:content");
341     boolean setReplicationAttributes = isContent && markReplicationActivated;
342 
343     List<Attribute> attributes = new ArrayList<>(element.getAttributes());
344     for (Attribute attribute : attributes) {
345       boolean excluded = false;
346       if (matches(attribute.getQualifiedName(), this.excludeProperties, false)) {
347         if (isRepositoryUserGroup && StringUtils.equals(attribute.getQualifiedName(), JcrConstants.JCR_UUID)) {
348           // keep jcr:uuid property for groups and users, otherwise they cannot be imported again
349         }
350         else {
351           attribute.detach();
352           excluded = true;
353         }
354       }
355       else if (StringUtils.equals(attribute.getQualifiedName(), PRIMARYTYPE_PROPERTY)) {
356         String namespacePrefix = StringUtils.substringBefore(attribute.getValue(), ":");
357         collectNamespacePrefix(namespacePrefixesActuallyUsed, namespacePrefix);
358       }
359       else if (StringUtils.equals(attribute.getQualifiedName(), MIXINS_PROPERTY)) {
360         String filteredValue = filterMixinsPropertyValue(attribute.getValue(), namespacePrefixesActuallyUsed);
361         if (StringUtils.isBlank(filteredValue)) {
362           attribute.detach();
363         }
364         else {
365           attribute.setValue(filteredValue);
366         }
367       }
368       else if (StringUtils.startsWith(attribute.getValue(), "{Name}")) {
369         collectNamespacePrefixNameArray(namespacePrefixesActuallyUsed, attribute.getQualifiedName(), attribute.getValue());
370         // alphabetically sort name values
371         attribute.setValue(sortReferenceValues(attribute.getQualifiedName(), attribute.getValue(), PropertyType.NAME));
372       }
373       else if (StringUtils.startsWith(attribute.getValue(), "{WeakReference}")) {
374         // alphabetically sort weak reference values
375         attribute.setValue(sortReferenceValues(attribute.getQualifiedName(), attribute.getValue(), PropertyType.WEAKREFERENCE));
376       }
377       if (!excluded) {
378         collectNamespacePrefix(namespacePrefixesActuallyUsed, attribute.getNamespacePrefix());
379       }
380     }
381 
382     // set replication status for jcr:content nodes inside cq:Page nodes
383     if (setReplicationAttributes && matches(path, markReplicationActivatedIncludeNodes, true)) {
384       addMixin(element, "cq:ReplicationStatus");
385       element.setAttribute("lastReplicated", "{Date}" + dateLastReplicated, CQ_NAMESPACE);
386       element.setAttribute("lastReplicationAction", "Activate", CQ_NAMESPACE);
387       collectNamespacePrefix(namespacePrefixesActuallyUsed, CQ_NAMESPACE.getPrefix());
388     }
389 
390     List<Element> children = new ArrayList<>(element.getChildren());
391     for (Element child : children) {
392       applyXmlExcludes(child, path, namespacePrefixesActuallyUsed, (insideReplicationElement || isReplicationElement) && !isContent);
393     }
394   }
395 
396   private String filterMixinsPropertyValue(String value, Set<String> namespacePrefixesActuallyUsed) {
397     if (this.excludeMixins.length == 0 || StringUtils.isBlank(value)) {
398       return value;
399     }
400 
401     DocViewProperty prop = DocViewProperty.parse(MIXINS_PROPERTY, value);
402     List<Value> mixins = new ArrayList<>();
403     for (int i = 0; i < prop.values.length; i++) {
404       String mixin = prop.values[i];
405       if (!matches(mixin, this.excludeMixins, false)) {
406         String namespacePrefix = StringUtils.substringBefore(mixin, ":");
407         collectNamespacePrefix(namespacePrefixesActuallyUsed, namespacePrefix);
408         mixins.add(new MockValue(mixin, PropertyType.STRING));
409       }
410     }
411 
412     if (mixins.isEmpty()) {
413       return null;
414     }
415 
416     try {
417       return DocViewProperty.format(new MockProperty(MIXINS_PROPERTY, true, mixins.toArray(new Value[0])));
418     }
419     catch (RepositoryException ex) {
420       throw new RuntimeException("Unable to format value for " + MIXINS_PROPERTY, ex);
421     }
422   }
423 
424   private void addMixin(Element element, String mixin) {
425     String mixinsString = element.getAttributeValue("mixinTypes", JCR_NAMESPACE);
426 
427     List<String> mixins = new ArrayList<>();
428     if (!StringUtils.isBlank(mixinsString)) {
429       DocViewProperty prop = DocViewProperty.parse(MIXINS_PROPERTY, mixinsString);
430       for (int i = 0; i < prop.values.length; i++) {
431         mixins.add(prop.values[i]);
432       }
433     }
434     if (!mixins.contains(mixin)) {
435       mixins.add(mixin);
436     }
437 
438     try {
439       Value[] values = mixins.stream()
440           .map(item -> new MockValue(item, PropertyType.STRING))
441           .toArray(size -> new Value[size]);
442       mixinsString = DocViewProperty.format(new MockProperty(MIXINS_PROPERTY, true, values));
443       element.setAttribute("mixinTypes", mixinsString, JCR_NAMESPACE);
444     }
445     catch (RepositoryException ex) {
446       throw new RuntimeException("Unable to format value for " + MIXINS_PROPERTY, ex);
447     }
448   }
449 
450   private void collectNamespacePrefix(Set<String> prefixes, String prefix) {
451     if (StringUtils.isNotBlank(prefix)) {
452       prefixes.add(prefix);
453     }
454   }
455 
456   private void collectNamespacePrefixNameArray(Set<String> prefixes, String name, String value) {
457     DocViewProperty prop = DocViewProperty.parse(name, value);
458     for (int i = 0; i < prop.values.length; i++) {
459       String item = prop.values[i];
460       String namespacePrefix = StringUtils.substringBefore(item, ":");
461       collectNamespacePrefix(prefixes, namespacePrefix);
462     }
463   }
464 
465   /**
466    * Sort weak reference values alphabetically to ensure consistent ordering.
467    * @param name Property name
468    * @param value Property value
469    * @param propertyType Property type from {@link PropertyType}
470    * @return Property value with sorted references
471    */
472   private String sortReferenceValues(String name, String value, int propertyType) {
473     Set<String> refs = new TreeSet<>();
474     DocViewProperty prop = DocViewProperty.parse(name, value);
475     for (int i = 0; i < prop.values.length; i++) {
476       refs.add(prop.values[i]);
477     }
478     List<Value> values = new ArrayList<>();
479     for (String ref : refs) {
480       values.add(new MockValue(ref, propertyType));
481     }
482     try {
483       return DocViewProperty.format(new MockProperty(name, true, values.toArray(new Value[0])));
484     }
485     catch (RepositoryException ex) {
486       throw new RuntimeException("Unable to format value for " + name, ex);
487     }
488   }
489 
490   /**
491    * Mock implementations of JCR property and value to be handed over to {@link DocViewProperty#format(Property)}
492    * method.
493    */
494   private static class MockProperty implements Property, PropertyDefinition {
495 
496     private final String name;
497     private final boolean multiple;
498     private final Value[] values;
499 
500     MockProperty(String name, boolean multiple, Value[] values) {
501       this.name = name;
502       this.multiple = multiple;
503       this.values = values;
504     }
505 
506     @Override
507     public String getName() {
508       return name;
509     }
510 
511     @Override
512     public int getType() {
513       if (values.length > 0) {
514         return values[0].getType();
515       }
516       return PropertyType.UNDEFINED;
517     }
518 
519     @Override
520     public boolean isMultiple() {
521       return multiple;
522     }
523 
524     @Override
525     public Value getValue() throws ValueFormatException {
526       if (multiple) {
527         throw new ValueFormatException("Property is multiple.");
528       }
529       return values[0];
530     }
531 
532     @Override
533     public Value[] getValues() throws ValueFormatException {
534       if (!multiple) {
535         throw new ValueFormatException("Property is not multiple.");
536       }
537       return values;
538     }
539 
540     @Override
541     public PropertyDefinition getDefinition() {
542       return this;
543     }
544 
545 
546     // -- unsupported methods --
547 
548     @Override
549     public String getPath() {
550       throw new UnsupportedOperationException();
551     }
552 
553     @Override
554     public Item getAncestor(int depth) {
555       throw new UnsupportedOperationException();
556     }
557 
558     @Override
559     public Node getParent() {
560       throw new UnsupportedOperationException();
561     }
562 
563     @Override
564     public int getDepth() {
565       throw new UnsupportedOperationException();
566     }
567 
568     @Override
569     public Session getSession() {
570       throw new UnsupportedOperationException();
571     }
572 
573     @Override
574     public boolean isNode() {
575       throw new UnsupportedOperationException();
576     }
577 
578     @Override
579     public boolean isNew() {
580       throw new UnsupportedOperationException();
581     }
582 
583     @Override
584     public boolean isModified() {
585       throw new UnsupportedOperationException();
586     }
587 
588     @Override
589     public boolean isSame(Item otherItem) {
590       throw new UnsupportedOperationException();
591     }
592 
593     @Override
594     public void accept(ItemVisitor visitor) {
595       throw new UnsupportedOperationException();
596     }
597 
598     @Override
599     public void save() {
600       throw new UnsupportedOperationException();
601     }
602 
603     @Override
604     public void refresh(boolean keepChanges) {
605       throw new UnsupportedOperationException();
606     }
607 
608     @Override
609     public void remove() {
610       throw new UnsupportedOperationException();
611     }
612 
613     @Override
614     public void setValue(Value value) {
615       throw new UnsupportedOperationException();
616     }
617 
618     @Override
619     public void setValue(Value[] value) {
620       throw new UnsupportedOperationException();
621     }
622 
623     @Override
624     public void setValue(String value) {
625       throw new UnsupportedOperationException();
626     }
627 
628     @Override
629     public void setValue(String[] value) {
630       throw new UnsupportedOperationException();
631     }
632 
633     @Override
634     public void setValue(InputStream value) {
635       throw new UnsupportedOperationException();
636     }
637 
638     @Override
639     public void setValue(Binary value) {
640       throw new UnsupportedOperationException();
641     }
642 
643     @Override
644     public void setValue(long value) {
645       throw new UnsupportedOperationException();
646     }
647 
648     @Override
649     public void setValue(double value) {
650       throw new UnsupportedOperationException();
651     }
652 
653     @Override
654     public void setValue(BigDecimal value) {
655       throw new UnsupportedOperationException();
656     }
657 
658     @Override
659     public void setValue(Calendar value) {
660       throw new UnsupportedOperationException();
661     }
662 
663     @Override
664     public void setValue(boolean value) {
665       throw new UnsupportedOperationException();
666     }
667 
668     @Override
669     public void setValue(Node value) {
670       throw new UnsupportedOperationException();
671     }
672 
673     @Override
674     public String getString() {
675       throw new UnsupportedOperationException();
676     }
677 
678     @Override
679     public InputStream getStream() {
680       throw new UnsupportedOperationException();
681     }
682 
683     @Override
684     public Binary getBinary() {
685       throw new UnsupportedOperationException();
686     }
687 
688     @Override
689     public long getLong() {
690       throw new UnsupportedOperationException();
691     }
692 
693     @Override
694     public double getDouble() {
695       throw new UnsupportedOperationException();
696     }
697 
698     @Override
699     public BigDecimal getDecimal() {
700       throw new UnsupportedOperationException();
701     }
702 
703     @Override
704     public Calendar getDate() {
705       throw new UnsupportedOperationException();
706     }
707 
708     @Override
709     public boolean getBoolean() {
710       throw new UnsupportedOperationException();
711     }
712 
713     @Override
714     public Node getNode() {
715       throw new UnsupportedOperationException();
716     }
717 
718     @Override
719     public Property getProperty() {
720       throw new UnsupportedOperationException();
721     }
722 
723     @Override
724     public long getLength() {
725       throw new UnsupportedOperationException();
726     }
727 
728     @Override
729     public long[] getLengths() {
730       throw new UnsupportedOperationException();
731     }
732 
733     @Override
734     public NodeType getDeclaringNodeType() {
735       throw new UnsupportedOperationException();
736     }
737 
738     @Override
739     public boolean isAutoCreated() {
740       throw new UnsupportedOperationException();
741     }
742 
743     @Override
744     public boolean isMandatory() {
745       throw new UnsupportedOperationException();
746     }
747 
748     @Override
749     public int getOnParentVersion() {
750       throw new UnsupportedOperationException();
751     }
752 
753     @Override
754     public boolean isProtected() {
755       throw new UnsupportedOperationException();
756     }
757 
758     @Override
759     public int getRequiredType() {
760       throw new UnsupportedOperationException();
761     }
762 
763     @Override
764     public String[] getValueConstraints() {
765       throw new UnsupportedOperationException();
766     }
767 
768     @Override
769     public Value[] getDefaultValues() {
770       throw new UnsupportedOperationException();
771     }
772 
773     @Override
774     public String[] getAvailableQueryOperators() {
775       throw new UnsupportedOperationException();
776     }
777 
778     @Override
779     public boolean isFullTextSearchable() {
780       throw new UnsupportedOperationException();
781     }
782 
783     @Override
784     public boolean isQueryOrderable() {
785       throw new UnsupportedOperationException();
786     }
787 
788   }
789 
790   private static class MockValue implements Value {
791 
792     private final String value;
793     private final int type;
794 
795     MockValue(String value, int type) {
796       this.value = value;
797       this.type = type;
798     }
799 
800     @Override
801     public String getString() {
802       return value;
803     }
804 
805     @Override
806     public int getType() {
807       return type;
808     }
809 
810 
811     // -- unsupported methods --
812 
813     @Override
814     public InputStream getStream() {
815       throw new UnsupportedOperationException();
816     }
817 
818     @Override
819     public Binary getBinary() {
820       throw new UnsupportedOperationException();
821     }
822 
823     @Override
824     public long getLong() {
825       throw new UnsupportedOperationException();
826     }
827 
828     @Override
829     public double getDouble() {
830       throw new UnsupportedOperationException();
831     }
832 
833     @Override
834     public BigDecimal getDecimal() {
835       throw new UnsupportedOperationException();
836     }
837 
838     @Override
839     public Calendar getDate() {
840       throw new UnsupportedOperationException();
841     }
842 
843     @Override
844     public boolean getBoolean() {
845       throw new UnsupportedOperationException();
846     }
847 
848   }
849 
850 }