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 }