001    /*
002     * $Revision: 15115 $ 
003     * $Date: 2009-04-29 11:01:56 +0200 (Wed, 29 Apr 2009) $
004     *
005     * This file is part of ***  M y C o R e  ***
006     * See http://www.mycore.de/ for details.
007     *
008     * This program is free software; you can use it, redistribute it
009     * and / or modify it under the terms of the GNU General Public License
010     * (GPL) as published by the Free Software Foundation; either version 2
011     * of the License or (at your option) any later version.
012     *
013     * This program is distributed in the hope that it will be useful, but
014     * WITHOUT ANY WARRANTY; without even the implied warranty of
015     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016     * GNU General Public License for more details.
017     *
018     * You should have received a copy of the GNU General Public License
019     * along with this program, in a file called gpl.txt or license.txt.
020     * If not, write to the Free Software Foundation Inc.,
021     * 59 Temple Place - Suite 330, Boston, MA  02111-1307 USA
022     */
023    
024    package org.mycore.frontend.editor;
025    
026    import java.io.ByteArrayOutputStream;
027    import java.util.HashMap;
028    import java.util.HashSet;
029    import java.util.Iterator;
030    import java.util.List;
031    import java.util.Properties;
032    import java.util.Set;
033    
034    import org.apache.log4j.Logger;
035    import org.jdom.Attribute;
036    import org.jdom.Content;
037    import org.jdom.Document;
038    import org.jdom.Element;
039    import org.jdom.filter.ElementFilter;
040    import org.jdom.output.Format;
041    import org.jdom.output.XMLOutputter;
042    import org.mycore.common.MCRConfigurationException;
043    import org.mycore.common.MCRConstants;
044    import org.mycore.common.xml.MCRURIResolver;
045    import org.mycore.common.xml.MCRXMLHelper;
046    
047    /*
048     * Reads in definition of editor forms like search mask and data input forms.
049     * Resolves includes and prepares editor form for output. 
050     */
051    public class MCREditorDefReader 
052    {
053      private final static Logger LOGGER = Logger.getLogger( MCREditorDefReader.class );
054      
055      private Element editor; 
056      
057      HashMap<String,Element> id2component = new HashMap<String,Element>();
058      HashMap<Element,String> referencing2ref = new HashMap<Element,String>();
059    
060      /**
061       * Reads the editor definition from the given URI
062       * 
063       * @param validate
064       *            if true, validate editor definition against schema
065       */
066      MCREditorDefReader( String uri, String ref, boolean validate ) 
067      {
068        long time = System.nanoTime();
069        
070        Element include = new Element( "include" ).setAttribute( "uri", uri );
071        if( ( ref != null ) && ( ref.length() > 0 ) ) include.setAttribute( "ref", ref );
072        
073        editor = new Element( "editor" );
074        editor.setAttribute( "id", ref );
075        editor.addContent( include );
076        resolveIncludes( editor );
077        checkDuplicateIDs( editor );
078        resolveReferences();
079        if( validate ) validate( uri, ref );
080        
081        time =  ( System.nanoTime() - time ) / 1000000;
082        LOGGER.info( "Finished reading editor definition in " + time + " ms" );
083      }
084      
085      private void checkDuplicateIDs(Element editor) {
086            Set<String> ids = new HashSet<String>();
087            Iterator<Element> elements = editor.getDescendants(new ElementFilter());
088            while (elements.hasNext()) {
089                String id = elements.next().getAttributeValue("id");
090                if ((id == null) || (id.trim().length() == 0))
091                    continue;
092                if (ids.contains(id)) {
093                    String msg = "Duplicate ID '" + id + "', already used in editor definition";
094                    throw new MCRConfigurationException(msg);
095                } else
096                    ids.add(id);
097            }
098        }
099      
100      private void validate( String uri, String ref )
101      {
102        if( ( ref != null ) && ( ref.length() > 0 ) ) uri += "#" + ref;
103        LOGGER.info( "Validating editor " + uri + "..." );
104        
105        Document doc = new Document( editor );
106        editor.setAttribute( "noNamespaceSchemaLocation", "editor.xsd", MCRConstants.XSI_NAMESPACE );
107    
108        XMLOutputter xout = new XMLOutputter();
109        xout.setFormat( Format.getPrettyFormat() );
110        ByteArrayOutputStream baos = new ByteArrayOutputStream();
111        try 
112        {
113          xout.output( doc, baos );
114          baos.close();
115          MCRXMLHelper.parseXML( baos.toByteArray(), true );
116        } 
117        catch( Exception ex ) 
118        {
119          String msg = "Error validating editor " + uri;
120          LOGGER.error( msg );
121          throw new MCRConfigurationException( msg, ex );
122        }
123    
124        editor.detach();
125        editor.removeAttribute("noNamespaceSchemaLocation", MCRConstants.XSI_NAMESPACE);
126        LOGGER.info( "Validation succeeded." );
127      }
128    
129      /**
130       * Returns the complete editor with all references resolved 
131       */
132      Element getEditor()
133      { return editor; }
134      
135      /**
136       * Recursively removes include elements that are direct or indirect children
137       * of the given container element and replaces them with the included
138       * resource. Includes that may be contained in included resources are
139       * recursively resolved, too.
140       * 
141       * @param element
142       *            The element where to start resolving includes
143       */
144      private boolean resolveIncludes( Element element )
145      {
146        boolean replaced = false;
147        
148        String ref = element.getAttributeValue( "ref", "" );
149        if( element.getName().equals( "include" ) )
150        {
151          String uri = element.getAttributeValue( "uri" );
152          if( uri != null )
153          {
154            LOGGER.info( "Including " + uri + ( (ref.length() > 0) ? "#" + ref : "" ) ); 
155            Element parent = element.getParentElement();
156            int pos = parent.indexOf( element );
157          
158            Element container = MCRURIResolver.instance().resolve( uri );
159            List<Content> found;
160            
161            if( ref.length() == 0 )
162              found = container.cloneContent();
163            else
164            {
165              found = findContent( container, ref );
166              ref = "";
167            }
168            replaced = true;
169            parent.addContent( pos, found );
170            element.detach();
171          }
172        }
173        else
174        {
175          String id = element.getAttributeValue( "id", "" );
176          if( id.length() > 0 ) id2component.put( id, element );
177          
178          setDefaultAttributes( element );
179          resolveChildren( element );
180        }
181    
182        if( ref.length() > 0 ) referencing2ref.put( element, ref );
183        return replaced;
184      }
185      
186      private void resolveChildren( Element parent )
187      {
188        for( int i = 0; i < parent.getContentSize(); i++ )
189        {
190          Content child = parent.getContent( i );
191          if( ( child instanceof Element ) && resolveIncludes( (Element)child ) ) i--;
192        }
193      }
194    
195      private List<Content> findContent( Element candidate, String id )
196      {
197        if( id.equals( candidate.getAttributeValue( "id" ) ) )
198          return candidate.cloneContent();
199        else
200        {
201          for( Element child : (List<Element>)( candidate.getChildren() ) )
202          { 
203            List<Content> found = findContent( child, id );
204            if( found != null ) return found;
205          }
206          return null;
207        }
208      }
209      
210      /**
211       * Returns that direct or indirect child element of the given element, thats
212       * ID attribute has the given value.
213       * 
214       * @param id
215       *            the value the ID attribute must have
216       * @param candidate
217       *            the element to start searching with
218       * @return the element below that has the given ID, or null if no such
219       *         element exists.
220       */
221      static Element findElementByID( String id, Element candidate )
222      {
223        if( id.equals( candidate.getAttributeValue( "id" ) ) )
224          return candidate;
225        else
226        {
227          for( Element child : (List<Element>)( candidate.getChildren() ) )
228          { 
229            Element found = findElementByID( id, child );
230            if( found != null ) return found;
231          }
232          return null;
233        }
234      }
235      
236      /**
237       * Recursively resolves references by the @ref attribute and
238       * replaces them with the referenced component.
239       */
240      private void resolveReferences()
241      {
242        for( Iterator it = referencing2ref.keySet().iterator() ; it.hasNext(); )
243        {
244          Element referencing = (Element)(it.next());
245          String id = referencing2ref.get( referencing );
246          LOGGER.debug( "Resolving reference to " + id ); 
247    
248          Element found = id2component.get( id );
249          if( found == null )
250          {
251            String msg = "Reference to component " + id + " could not be resolved";
252            throw new MCRConfigurationException( msg );
253          }
254          
255          String name = referencing.getName();
256          referencing.removeAttribute( "ref" );
257          it.remove();
258          
259          if( name.equals( "cell" ) || name.equals( "repeater" ) )
260          {
261            if( found.getParentElement().getName().equals( "components" ) )
262              referencing.addContent( 0, found.detach() );
263            else
264              referencing.addContent( 0, (Element)(found.clone()) );
265          }
266          else if( name.equals( "panel" ) )
267          {
268            if( referencing2ref.containsValue( id ) )
269              referencing.addContent( 0, found.cloneContent() );
270            else
271            {
272              found.detach();
273              List<Content> content = found.getContent();
274              for( int i = 0; ! content.isEmpty(); i++ )
275              {
276                Content child = content.remove( 0 );
277                referencing.addContent( i, child );
278              }
279            }
280          }
281          else if( name.equals( "include" ) )
282          {
283            Element parent = referencing.getParentElement();
284            int pos = parent.indexOf( referencing );
285            referencing.detach();
286            
287            if( referencing2ref.containsValue( id ) )
288              parent.addContent( pos, found.cloneContent() );
289            else
290            {
291              found.detach();
292              List<Content> content = found.getContent();
293              for( int i = pos; ! content.isEmpty(); i++ )
294              {
295                Content child = content.remove( 0 );
296                parent.addContent( i, child );
297              }
298            }
299          }
300        }
301    
302        Element components = editor.getChild( "components" );
303        String root = components.getAttributeValue( "root" );
304        
305        for( int i = 0; i < components.getContentSize(); i++ )
306        {
307          Content child = components.getContent( i );
308          if( ! ( child instanceof Element ) ) continue;
309          if( ((Element)child).getName().equals( "headline" ) ) continue;
310          if( ! root.equals( ((Element)child).getAttributeValue( "id" ) ) )
311            components.removeContent( i-- );
312        }  
313      }
314    
315      /**
316       * This map contains default attribute values to set for a given element name
317       */
318      private static HashMap<String,Properties> defaultAttributes = new HashMap<String,Properties>();
319      
320      static
321      {
322        defaultAttributes.put( "cell", new Properties() );
323        defaultAttributes.get( "cell" ).setProperty( "row", "1" );
324        defaultAttributes.get( "cell" ).setProperty( "col", "1" );
325        defaultAttributes.get( "cell" ).setProperty( "class", "editorCell" );
326        defaultAttributes.put( "headline", new Properties() );
327        defaultAttributes.get( "headline" ).setProperty( "class", "editorHeadline" );
328        defaultAttributes.put( "repeater", new Properties() );
329        defaultAttributes.get( "repeater" ).setProperty( "class", "editorRepeater" );
330        defaultAttributes.get( "repeater" ).setProperty( "min", "1" );
331        defaultAttributes.get( "repeater" ).setProperty( "max", "10" );
332        defaultAttributes.put( "panel", new Properties() );
333        defaultAttributes.get( "panel" ).setProperty( "class", "editorPanel" );
334        defaultAttributes.put( "editor", new Properties() );
335        defaultAttributes.get( "editor" ).setProperty( "class", "editor" );
336        defaultAttributes.put( "helpPopup", new Properties() );
337        defaultAttributes.get( "helpPopup" ).setProperty( "class", "editorButton" );
338        defaultAttributes.put( "text", new Properties() );
339        defaultAttributes.get( "text" ).setProperty( "class", "editorText" );
340        defaultAttributes.put( "textfield", new Properties() );
341        defaultAttributes.get( "textfield" ).setProperty( "class", "editorTextfield" );
342        defaultAttributes.put( "textarea", new Properties() );
343        defaultAttributes.get( "textarea" ).setProperty( "class", "editorTextarea" );
344        defaultAttributes.put( "file", new Properties() );
345        defaultAttributes.get( "file" ).setProperty( "class", "editorFile" );
346        defaultAttributes.put( "password", new Properties() );
347        defaultAttributes.get( "password" ).setProperty( "class", "editorPassword" );
348        defaultAttributes.put( "subselect", new Properties() );
349        defaultAttributes.get( "subselect" ).setProperty( "class", "editorButton" );
350        defaultAttributes.put( "submitButton", new Properties() );
351        defaultAttributes.get( "submitButton" ).setProperty( "class", "editorButton" );
352        defaultAttributes.put( "cancelButton", new Properties() );
353        defaultAttributes.get( "cancelButton" ).setProperty( "class", "editorButton" );
354        defaultAttributes.put( "button", new Properties() );
355        defaultAttributes.get( "button" ).setProperty( "class", "editorButton" );
356        defaultAttributes.put( "list", new Properties() );
357        defaultAttributes.get( "list" ).setProperty( "class", "editorList" );
358        defaultAttributes.put( "checkbox", new Properties() );
359        defaultAttributes.get( "checkbox" ).setProperty( "class", "editorCheckbox" );
360      }
361      
362      /**
363       * Sets default attribute values for the given element, if any
364       */
365      private void setDefaultAttributes( Element element ) 
366      {
367        Properties defaults = defaultAttributes.get( element.getName() );
368        if( defaults == null ) return;
369        for( Iterator it = defaults.keySet().iterator() ; it.hasNext(); )
370        {
371          String key = (String)( it.next() );
372          if( element.getAttribute( key )  == null )
373            element.setAttribute( key, defaults.getProperty( key ) );
374        }
375      }
376    
377      /** 
378       * Transforms @var attribute values that have a condition like
379       * title[@type='main'] into escaped internal syntax
380       * title__type__main 
381       */
382      static void fixConditionedVariables( Element editor )
383      {
384        for( Iterator iter = editor.getDescendants(new ElementFilter()); iter.hasNext(); )
385        {
386          Element element = (Element) (iter.next());
387          String var = element.getAttributeValue( "var", "" ); 
388          if( var.contains( "[@" ) )
389          {
390            var = var.replace( "[@", "__" );
391            var = var.replace( "='", "__" );
392            var = var.replace( "']", "" );
393            element.setAttribute( "var", var );
394          } 
395        }
396      }
397    }